feat(hub-guide): expand plugin with 26 best-practice skills, hooks, and references
Transform hub-guide from a single-skill Hub monorepo guide into a comprehensive programming best-practice plugin covering all situations. Skills (26): - Core: engineering-principles, clean-code, clean-architecture, design-patterns, testing, error-handling, security, api-design, git-workflow, documentation, logging-observability, performance - Languages: typescript, python, rust, go - Frameworks: react-frontend, elysiajs, hono-backend, drizzle-database, nextjs - Infrastructure: docker, ci-cd, monitoring - Monorepo: monorepo, hub-guide (existing) Hooks: - SessionStart: auto-detect project type and activate relevant skills - PreToolUse (Write|Edit): inject language-specific rules per file type Reference files for deep dives: - clean-architecture/references/solid.md (SOLID + component principles) - design-patterns/references/catalog.md (full GoF catalog with examples) - testing/references/mocks.md (test double taxonomy) Restructure plugin to modern skills/ directory format.
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
---
|
||||
name: api-design
|
||||
description: Best practices for API design — REST, GraphQL, RPC conventions, versioning, status codes, pagination, error responses, and documentation. Use when designing new endpoints, reviewing API contracts, or whenever the user mentions "API design," "REST," "RESTful," "GraphQL," "endpoint," "status code," "pagination," "API versioning," "OpenAPI," "gRPC," or "API contract."
|
||||
---
|
||||
|
||||
# API Design
|
||||
|
||||
## REST Conventions
|
||||
|
||||
### URL Structure
|
||||
```
|
||||
POST /resources # Create
|
||||
GET /resources # List (with query params for filtering/pagination)
|
||||
GET /resources/:id # Read one
|
||||
PUT /resources/:id # Full replace
|
||||
PATCH /resources/:id # Partial update
|
||||
DELETE /resources/:id # Delete
|
||||
GET /resources/:id/related # Nested resource
|
||||
```
|
||||
|
||||
- **Plural nouns**, not verbs: `/users` not `/getUsers`
|
||||
- **Kebab-case** for multi-word: `/order-items` not `/orderItems`
|
||||
- **No file extensions**: `/users` not `/users.json`
|
||||
- **Version in URL or Accept header**: `/v1/users` or `Accept: application/vnd.api.v1+json`
|
||||
|
||||
### Response Format
|
||||
```json
|
||||
// Success
|
||||
HTTP 200
|
||||
{ "data": { "id": "1", "name": "Alice", "email": "alice@example.com" } }
|
||||
|
||||
// List with pagination
|
||||
HTTP 200
|
||||
{
|
||||
"data": [ ... ],
|
||||
"meta": { "total": 100, "page": 1, "per_page": 20 },
|
||||
"links": { "self": "?page=1", "next": "?page=2", "prev": null }
|
||||
}
|
||||
|
||||
// Error
|
||||
HTTP 422
|
||||
{
|
||||
"error": {
|
||||
"code": "VALIDATION_ERROR",
|
||||
"message": "Email is required",
|
||||
"details": [{ "field": "email", "issue": "required" }]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Status Codes — Use Precisely
|
||||
|
||||
| Code | When |
|
||||
|------|------|
|
||||
| **200** | Success (GET, PUT, PATCH) |
|
||||
| **201** | Created (POST) |
|
||||
| **204** | No Content (DELETE success) |
|
||||
| **301** | Moved permanently (redirect) |
|
||||
| **400** | Bad request (malformed input) |
|
||||
| **401** | Unauthenticated (missing/invalid credentials) |
|
||||
| **403** | Forbidden (authenticated but not authorized) |
|
||||
| **404** | Not found |
|
||||
| **409** | Conflict (duplicate, version mismatch) |
|
||||
| **422** | Unprocessable entity (validation failure) |
|
||||
| **429** | Too many requests (rate limit) |
|
||||
| **500** | Internal server error (unhandled, doesn't fit above) |
|
||||
| **502/503** | Upstream / service unavailable |
|
||||
|
||||
Never return 200 with an error body. Never return 500 for validation errors.
|
||||
|
||||
### Pagination
|
||||
```typescript
|
||||
// Cursor-based (preferred for live data)
|
||||
GET /items?cursor=abc123&limit=20
|
||||
Response: { data: [], meta: { next_cursor: "xyz789", has_more: true } }
|
||||
|
||||
// Page-based (fine for stable datasets)
|
||||
GET /items?page=1&per_page=20
|
||||
Response: { data: [], meta: { total: 100, page: 1, per_page: 20 } }
|
||||
```
|
||||
|
||||
### Filtering, Sorting, Fields
|
||||
```typescript
|
||||
GET /items?filter[status]=active&filter[created_at]=2024-01-01..2024-12-31
|
||||
GET /items?sort=-created_at,name // -prefix = descending
|
||||
GET /items?fields=id,name,status // sparse fieldset (performance)
|
||||
```
|
||||
|
||||
## GraphQL
|
||||
|
||||
- **Schema-first** — design the schema before implementing resolvers.
|
||||
- **N+1 problem** — use DataLoader for batch loading.
|
||||
- **Expose a single endpoint** — no per-resource URLs.
|
||||
- **Mutations return the mutated object** — always include the affected type.
|
||||
- **Paginate connections** — use the Relay Connection spec (`edges { node }`).
|
||||
|
||||
## API Versioning
|
||||
|
||||
- **URL versioning** (`/v1/users`) — simplest, most explicit.
|
||||
- **Header versioning** (`Accept: application/vnd.api.v2+json`) — cleaner URL.
|
||||
- **Never remove fields** — deprecate first, remove in next major version.
|
||||
- **Document breaking changes** — changelog, migration guide, sunset header (`Sunset: Sat, 1 Nov 2025 00:00:00 GMT`).
|
||||
|
||||
## API Documentation
|
||||
|
||||
- **OpenAPI 3.x** for REST APIs. Generate from code (Hono Zod OpenAPI, FastAPI).
|
||||
- **Include in docs:** endpoint, method, params, request body schema, response schema, error codes, auth requirement, example requests/responses.
|
||||
- **Keep a changelog** — versioned alongside the API spec.
|
||||
|
||||
## Other API Styles
|
||||
|
||||
### gRPC
|
||||
- Use for internal service-to-service communication.
|
||||
- Proto3, HTTP/2, streaming support.
|
||||
- Better than REST for high-throughput, low-latency internal APIs.
|
||||
|
||||
### RPC-style (tRPC, Elysia Eden)
|
||||
- No URL routing — direct function calls from client.
|
||||
- Type-safe end-to-end. Preferred for full-stack TypeScript.
|
||||
- Trade-off: couples client and server types.
|
||||
|
||||
## Idempotency
|
||||
|
||||
- **PUT and DELETE are idempotent** — same request multiple times = same result.
|
||||
- **POST is not idempotent** — provide `Idempotency-Key` header for payment-like operations.
|
||||
- **PATCH can be idempotent** if you send the full delta (not increments).
|
||||
|
||||
## API Anti-patterns
|
||||
|
||||
- ❌ **Leaking internal implementation** — exposing DB fields, raw SQL, internal IDs
|
||||
- ❌ **Inconsistent error format** — sometimes `{error}`, sometimes `{message}`, sometimes `{errors}[]`
|
||||
- ❌ **Over-fetching/n+1** — returning more data than needed, making N+1 queries
|
||||
- ❌ **No rate limit info** — no `Retry-After`, no `X-RateLimit-*` headers
|
||||
- ❌ **Version in body** — `/api?version=2`, version field in JSON body
|
||||
- ❌ **200 for errors** — `HTTP 200 {"error": "not found"}` should be `HTTP 404`
|
||||
@@ -0,0 +1,168 @@
|
||||
---
|
||||
name: ci-cd
|
||||
description: CI/CD best practices — GitHub Actions, pipeline design, Docker build + push, deployment workflows, testing in CI, and environment management. Use when designing CI/CD pipelines, debugging workflow failures, or whenever the user mentions "CI," "CD," "GitHub Actions," "GitLab CI," "Jenkins," "pipeline," "deploy," "workflow," "Docker build," "environment," or "automation."
|
||||
---
|
||||
|
||||
# CI/CD Best Practices
|
||||
|
||||
## Pipeline Design Principles
|
||||
|
||||
1. **Fast feedback** — failing fast is better than failing comprehensively.
|
||||
2. **Deterministic** — same commit = same result, same artifacts.
|
||||
3. **Immutable artifacts** — build once, promote through environments.
|
||||
4. **Idempotent deployments** — deploying the same artifact again produces the same result.
|
||||
5. **Security gates** — scan dependencies, secrets, and code before production.
|
||||
|
||||
## GitHub Actions Structure
|
||||
|
||||
```
|
||||
.github/workflows/
|
||||
├── lint.yml # Quick checks — runs in <2 min
|
||||
├── docker-build-push.yml # Build + push images
|
||||
├── deploy-docker.yml # Deploy to VPS
|
||||
├── security.yml # CodeQL + dependency scan
|
||||
├── update-submodule.yml # Update submodule pointer
|
||||
└── ...other workflows
|
||||
```
|
||||
|
||||
### Workflow Patterns
|
||||
|
||||
**Lint (fast — gates everything else)**
|
||||
```yaml
|
||||
name: Lint
|
||||
on: [push, pull_request]
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
- run: bun install --frozen-lockfile
|
||||
- run: bun run ci # Biome lint + format
|
||||
```
|
||||
|
||||
**Build + Push (on push to main)**
|
||||
```yaml
|
||||
name: Build Docker
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths: ['apps/**', 'infra/**']
|
||||
repository_dispatch:
|
||||
types: [build]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
service: [scraper, hub]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: |
|
||||
docker build \
|
||||
-f infra/docker/${{ matrix.service }}.Dockerfile \
|
||||
-t ghcr.io/.../${{ matrix.service }}:sha-${{ github.sha }} \
|
||||
-t ghcr.io/.../${{ matrix.service }}:latest \
|
||||
.
|
||||
- run: docker push --all-tags ghcr.io/.../${{ matrix.service }}
|
||||
```
|
||||
|
||||
**Deploy (after build)**
|
||||
```yaml
|
||||
name: Deploy
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ['Build Docker']
|
||||
types: [completed]
|
||||
push:
|
||||
branches: [main]
|
||||
paths: ['infra/**']
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: appleboy/ssh-action@v1
|
||||
with:
|
||||
host: ${{ secrets.VPS_HOST }}
|
||||
username: ${{ secrets.VPS_USER }}
|
||||
key: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
script: |
|
||||
cd ${{ secrets.VPS_TARGET_DIR }}
|
||||
docker compose pull <service>
|
||||
docker compose up -d <service>
|
||||
```
|
||||
|
||||
## Key Patterns
|
||||
|
||||
### Matrix Builds
|
||||
```yaml
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
service: [scraper, hub, api]
|
||||
fail-fast: false # Let others complete even if one fails
|
||||
```
|
||||
|
||||
### Conditional Jobs
|
||||
```yaml
|
||||
jobs:
|
||||
deploy:
|
||||
if: github.event.workflow_run.conclusion == 'success'
|
||||
```
|
||||
|
||||
### Caching
|
||||
```yaml
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.bun/install/cache
|
||||
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-bun-
|
||||
```
|
||||
|
||||
### Secrets
|
||||
```yaml
|
||||
# All secrets in GitHub Secrets, never in code
|
||||
secrets:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
VPS_HOST: ${{ secrets.VPS_HOST }}
|
||||
ENV_FILE_PRODUCTION: ${{ secrets.ENV_FILE_PRODUCTION }}
|
||||
```
|
||||
|
||||
## Environment Strategy
|
||||
|
||||
| Environment | Purpose | Deploy Method |
|
||||
|-------------|---------|---------------|
|
||||
| `development` | Local dev | Manual `docker compose up` |
|
||||
| `staging` | Pre-production | Auto-deploy from PR branches |
|
||||
| `production` | Live | Auto-deploy from main |
|
||||
|
||||
## Quality Gates (run order)
|
||||
|
||||
1. **Lint** (<1 min) — Biome/Ruff/clippy + format check
|
||||
2. **Type check** (<2 min) — tsc/pyright/cargo check
|
||||
3. **Unit tests** (<3 min) — fast, no external deps
|
||||
4. **Build** (<5 min) — compile/transpile, build Docker images
|
||||
5. **Integration tests** (<10 min) — with DB, external services
|
||||
6. **Security scan** (<5 min) — CodeQL, dependency audit, secret scan
|
||||
7. **Deploy** (<2 min) — SSH, pull, restart
|
||||
|
||||
## Deployment (this repo's pattern)
|
||||
|
||||
1. SSH to VPS (`orangevps`)
|
||||
2. Pull latest images from GHCR
|
||||
3. Restart specific container (not all)
|
||||
4. Health check after restart
|
||||
5. Rollback if health check fails
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- ❌ **Large CI configs** — extract repeated blocks into actions. Use YAML anchors.
|
||||
- ❌ **Building in deployment** — build once in CI, deploy artifact. Avoid `docker compose build` on production.
|
||||
- ❌ **Hardcoded values** — use env vars, secrets, and GitHub variables.
|
||||
- ❌ **Skipping lint** — lint should run first and gate everything.
|
||||
- ❌ **No caching** — each run fetches deps fresh = 2x+ slower.
|
||||
- ❌ **Manual deployment steps** — automate everything. If it's manual, it will be wrong.
|
||||
- ❌ **Deploying untested artifacts** — run tests before build, not after.
|
||||
@@ -0,0 +1,94 @@
|
||||
---
|
||||
name: clean-architecture
|
||||
description: Apply Clean Architecture, hexagonal architecture, and SOLID principles when designing system boundaries, modules, or microservices. Use when structuring a new service, deciding what a component should own, untangling framework coupling, or whenever the user mentions "clean architecture," "hexagonal architecture," "onion architecture," "ports and adapters," "SOLID," "Dependency Rule," or "architecture boundaries."
|
||||
---
|
||||
|
||||
# Clean Architecture
|
||||
|
||||
Keep business rules independent of frameworks, databases, and UI.
|
||||
|
||||
## The Dependency Rule
|
||||
|
||||
**Source code dependencies must point inward.** Nothing in an inner circle can know about something in an outer circle.
|
||||
|
||||
```
|
||||
┌──────────────────────────────┐
|
||||
│ Framework / DB / UI / IO │ ← outer: frameworks, drivers, devices
|
||||
│ ┌──────────────────────────┐ │
|
||||
│ │ Interface Adapters │ │ ← presenters, controllers, gateways
|
||||
│ │ ┌──────────────────────┐ │ │
|
||||
│ │ │ Application (Use Cases) │ │ ← orchestrate business flows
|
||||
│ │ │ ┌──────────────────┐ │ │ │
|
||||
│ │ │ │ Domain / Entities│ │ │ │ ← pure business rules, no deps
|
||||
│ │ │ └──────────────────┘ │ │ │
|
||||
│ │ └──────────────────────┘ │ │
|
||||
│ └──────────────────────────┘ │
|
||||
└──────────────────────────────┘
|
||||
```
|
||||
|
||||
## Key Rules
|
||||
|
||||
1. **Domain layer** contains business entities and value objects. Zero framework imports. Zero database imports. Pure types and functions.
|
||||
2. **Application layer** contains use cases — orchestrate domain objects to fulfill business flows. Depends only on domain. Declares ports (interfaces) for IO.
|
||||
3. **Interface adapters** translate between use cases and the outside world — controllers, presenters, gateways. Depends on application layer + frameworks.
|
||||
4. **Infrastructure/Framework layer** implements the ports declared by the application layer — database repos, HTTP clients, message queues.
|
||||
5. **Screaming Architecture:** the project structure should scream "this is a [domain context]" — not "this is a Spring/Next.js/Django project."
|
||||
|
||||
## How to Check
|
||||
|
||||
- Can you swap the database without changing business logic? If not, boundary is violated.
|
||||
- Can you unit-test a use case without spinning up a framework? If not, your use case depends on infrastructure.
|
||||
- Do business entities import anything from the web framework or ORM? If so, revert that dependency.
|
||||
|
||||
## Practical Patterns
|
||||
|
||||
### Port-Adapter
|
||||
```typescript
|
||||
// Domain/Application port (declared here, implemented outside)
|
||||
interface UserRepository {
|
||||
findById(id: string): Promise<User | null>;
|
||||
}
|
||||
// Infrastructure adapter (implemented in infra layer)
|
||||
class PostgresUserRepository implements UserRepository { ... }
|
||||
```
|
||||
|
||||
### Use Case
|
||||
```typescript
|
||||
class CreateOrderUseCase {
|
||||
constructor(private readonly repo: OrderRepository) {}
|
||||
async execute(input: CreateOrderInput): Promise<Order> {
|
||||
const order = Order.create(input.items, input.customerId);
|
||||
return this.repo.save(order);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Dependency Injection
|
||||
Wire dependencies at the composition root — never in use cases or domain.
|
||||
```typescript
|
||||
// Composition Root
|
||||
const orderRepo = new PostgresOrderRepository(db);
|
||||
const createOrder = new CreateOrderUseCase(orderRepo);
|
||||
```
|
||||
|
||||
## SOLID (Quick Ref)
|
||||
|
||||
- **SRP:** A class has one reason to change (one actor).
|
||||
- **OCP:** Open for extension, closed for modification (polymorphism + strategy).
|
||||
- **LSP:** Subtypes must be substitutable for their base types.
|
||||
- **ISP:** Don't depend on interfaces you don't use (keep interfaces focused).
|
||||
- **DIP:** Depend on abstractions, not concretions. Business rules don't import frameworks.
|
||||
|
||||
## Deeper Reference
|
||||
|
||||
When the task calls for it, load:
|
||||
|
||||
- **[references/solid.md](references/solid.md)** — Full SOLID treatment (SRP, OCP, LSP, ISP, DIP). Component principles (REP, CCP, CRP, ADP, SDP, SAP). Examples for each principle, historical evolution, and practical tests for violations.
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- ❌ Business logic in route handlers or controllers
|
||||
- ❌ ORM entities directly exposed to the UI
|
||||
- ❌ Database queries mixed into use cases
|
||||
- ❌ Framework decorators on domain entities
|
||||
- ❌ "Everything is a CRUD" — missing use case layer
|
||||
@@ -0,0 +1,166 @@
|
||||
# SOLID Principles — Deep Reference
|
||||
|
||||
## Single Responsibility Principle (SRP)
|
||||
|
||||
> "A class should have one, and only one, reason to change." — Robert C. Martin
|
||||
|
||||
**Evolution of the definition:**
|
||||
- 2000 (PPP): "one reason to change"
|
||||
- 2008 (Clean Code): class does one thing
|
||||
- 2017 (Clean Architecture): "responsible to one, and only one, **actor**" — where actor is a person or tightly coupled group (e.g., accounting dept, HR dept, DevOps team)
|
||||
|
||||
### Practical Test
|
||||
If you cannot describe a module's responsibility in one sentence without "and," it violates SRP.
|
||||
|
||||
```typescript
|
||||
// ❌ Two actors: Accounting (calculatePay) + HR (save)
|
||||
class Employee {
|
||||
calculatePay(): Money { ... }
|
||||
save(): void { ... }
|
||||
}
|
||||
|
||||
// ✅ Separated by actor
|
||||
class EmployeePaymentCalc { calculatePay(emp: Employee): Money }
|
||||
class EmployeeRepository { save(emp: Employee): void }
|
||||
```
|
||||
|
||||
### When SRP Is Violated
|
||||
- Mixed persistence + business logic in the same class
|
||||
- A controller that validates, orchestrates, AND formats the response
|
||||
- A module that imports from both `domain/` and `infra/` packages
|
||||
|
||||
---
|
||||
|
||||
## Open/Closed Principle (OCP)
|
||||
|
||||
> "Software entities should be open for extension, closed for modification." — Bertrand Meyer
|
||||
|
||||
New behavior is added through **new code** (new classes, new modules), not by **editing existing, tested code**.
|
||||
|
||||
### Strategy Pattern (canonical OCP)
|
||||
```typescript
|
||||
// ❌ Closed for extension without modification
|
||||
function calculateDiscount(type: string, amount: number) {
|
||||
if (type === 'none') return 0;
|
||||
if (type === 'seasonal') return amount * 0.1;
|
||||
if (type === 'loyalty') return amount * 0.2;
|
||||
}
|
||||
|
||||
// ✅ Open for extension — add new strategy, never touch this code
|
||||
interface DiscountStrategy { apply(amount: number): number;
|
||||
class SeasonalDiscount implements DiscountStrategy { apply(a) { return a * 0.1 } }
|
||||
class LoyaltyDiscount implements DiscountStrategy { apply(a) { return a * 0.2 } }
|
||||
class DiscountCalculator {
|
||||
constructor(private strategies: DiscountStrategy[]) {}
|
||||
calculate(amount: number) { return this.strategies.reduce((acc, s) => acc + s.apply(amount), 0); }
|
||||
}
|
||||
```
|
||||
|
||||
### OCP Warning Signs
|
||||
- `if/else` or `switch` chains on a type/enum field
|
||||
- Feature toggles mixed into business logic (use plugin architecture)
|
||||
- Every new feature touches 5+ existing files
|
||||
|
||||
---
|
||||
|
||||
## Liskov Substitution Principle (LSP)
|
||||
|
||||
> "Objects of a superclass shall be replaceable with objects of its subclasses without breaking the system." — Barbara Liskov (1987)
|
||||
|
||||
**Revised (2020s):** "Subtypes must be substitutable for their base types." — applies to interfaces, protocols, and type parameters, not just class inheritance.
|
||||
|
||||
### The Square-Rectangle Problem (classic violation)
|
||||
```typescript
|
||||
class Rectangle { setWidth(w): void; setHeight(h): void }
|
||||
class Square extends Rectangle {
|
||||
setWidth(w) { super.setWidth(w); super.setHeight(w); } // Breaks caller's expectation
|
||||
}
|
||||
```
|
||||
|
||||
### Rules for Substitutability
|
||||
1. **Preconditions cannot be strengthened** in the subtype — subtype must accept everything the base accepts.
|
||||
2. **Postconditions cannot be weakened** — subtype must guarantee at least what the base guarantees.
|
||||
3. **Invariants must be preserved** — the base class's invariants must hold in the subtype.
|
||||
4. **History constraint** (Meyer): subtype methods cannot introduce state changes the base type wouldn't allow.
|
||||
|
||||
### LSP in Practice
|
||||
```typescript
|
||||
// Violation: PostgresUserRepo expects a table name, InMemoryUserRepo doesn't — not substitutable
|
||||
interface UserRepository {
|
||||
find(id: string): User;
|
||||
}
|
||||
class PostgresUserRepo implements UserRepository {
|
||||
constructor(private table: string) {} // extra constraint
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Interface Segregation Principle (ISP)
|
||||
|
||||
> "No client should be forced to depend on methods it does not use." — Robert C. Martin
|
||||
|
||||
Fat interfaces force implementors to stub out methods they don't need.
|
||||
|
||||
```typescript
|
||||
// ❌ Fat interface — forces every worker to implement onError, even if they never fail
|
||||
interface Worker { work(): void; eat(): void; onError(e: Error): void }
|
||||
|
||||
// ✅ Segregated — each interface has one job
|
||||
interface Workable { work(): void }
|
||||
interface Eatable { eat(): void }
|
||||
interface ErrorHandler { onError(e: Error): void }
|
||||
```
|
||||
|
||||
### When ISP Is Violated
|
||||
- A single interface has methods from different concerns (CRUD + reporting + admin)
|
||||
- Classes implement interface methods as `throw new UnsupportedOperationException`
|
||||
- Interface methods are unused in 80% of callers (consider splitting input vs output ports)
|
||||
|
||||
---
|
||||
|
||||
## Dependency Inversion Principle (DIP)
|
||||
|
||||
> "Abstractions should not depend on details. Details should depend on abstractions." — Robert C. Martin
|
||||
|
||||
**Not to be confused with Dependency Injection** (which is one way to implement DIP).
|
||||
|
||||
### High-level policy should not import low-level detail
|
||||
```typescript
|
||||
// ❌ High-level module depends on low-level detail
|
||||
class CreateOrderUseCase {
|
||||
private db = new PostgresConnection(); // violates DIP
|
||||
}
|
||||
|
||||
// ✅ Both depend on abstraction
|
||||
interface OrderRepository { save(order: Order): Promise<void> }
|
||||
class CreateOrderUseCase {
|
||||
constructor(private repo: OrderRepository) {} // depends on abstraction
|
||||
}
|
||||
class PostgresOrderRepo implements OrderRepository {} // detail depends on abstraction
|
||||
```
|
||||
|
||||
### The Dependency Rule (Clean Architecture)
|
||||
Source code dependencies point **inward** — nothing in an inner circle knows about something in an outer circle:
|
||||
- Domain → no imports from framework/infra/db
|
||||
- Application → imports domain, declares ports (interfaces)
|
||||
- Infrastructure → implements ports
|
||||
- Framework → wires everything at the composition root
|
||||
|
||||
---
|
||||
|
||||
## Component Principles (for larger systems)
|
||||
|
||||
### Cohesion Principles
|
||||
| Principle | Statement |
|
||||
|-----------|-----------|
|
||||
| **REP** (Reuse-Release Equivalence) | The unit of reuse is the unit of release |
|
||||
| **CCP** (Common Closure Principle) | Classes that change together belong together |
|
||||
| **CRP** (Common Reuse Principle) | Don't depend on things you don't use |
|
||||
|
||||
### Coupling Principles
|
||||
| Principle | Statement |
|
||||
|-----------|-----------|
|
||||
| **ADP** (Acyclic Dependencies Principle) | No cycles in the dependency graph |
|
||||
| **SDP** (Stable Dependencies Principle) | Depend in the direction of stability |
|
||||
| **SAP** (Stable Abstractions Principle) | Stable components should be abstract |
|
||||
@@ -0,0 +1,98 @@
|
||||
---
|
||||
name: clean-code
|
||||
description: Apply Robert C. Martin's (Uncle Bob's) Clean Code, Clean Architecture, and Clean Craftsmanship principles when writing, reviewing, or refactoring code. Use this skill whenever the user asks to write new code of non-trivial size, refactor existing code, review code for quality, design a module or system boundary, write tests, or whenever the user mentions "clean code," "clean architecture," "SOLID," "SRP," "OCP," "LSP," "ISP," "DIP," "TDD," "refactor," "code smells," or "Uncle Bob." Also engage proactively when producing code with poor naming, long functions (>20 lines), deep nesting, unclear abstractions, duplicated logic, switch/if-else chains on type, missing tests, or frameworks bleeding into business logic.
|
||||
---
|
||||
|
||||
# Clean Code
|
||||
|
||||
Core principles for writing readable, maintainable, and professional code.
|
||||
|
||||
## Philosophy
|
||||
|
||||
1. **Code is read far more than written** — ratio >10:1. Optimize for the reader.
|
||||
2. **Boy Scout Rule** — leave every module cleaner than you found it.
|
||||
3. **The only way to go fast is to go well.** Dirty code slows everyone down.
|
||||
|
||||
## 1. Meaningful Names
|
||||
|
||||
- Use **intention-revealing names** — what is it, why does it exist, how is it used?
|
||||
- **Avoid disinformation** — don't call it `accountList` unless it's a `List`. No `l`/`O` as variable names.
|
||||
- **Pronounceable, searchable names** — `genymdhms` is not acceptable.
|
||||
- **Class names** are nouns (`Customer`), **method names** are verbs (`postPayment`).
|
||||
- **One word per concept** — standardize `fetch` vs `retrieve` vs `get`.
|
||||
- **Ubiquitous language** — use the business domain's vocabulary consistently.
|
||||
|
||||
## 2. Functions
|
||||
|
||||
- **Small.** Target ~20 lines. If you can't see the whole function, it's too long.
|
||||
- **Do one thing.** Operational test: you cannot extract another function from it.
|
||||
- **One level of abstraction per function** — the Step-Down Rule.
|
||||
- **Few arguments.** 0 ideal, 1-2 fine, 3 suspect, 4+ → need a struct or a split.
|
||||
- **No flag arguments.** `render(true)` → split into `renderForSuite()` and `renderForSingleTest()`.
|
||||
- **No side effects.** A function named `checkPassword` must not also log a session.
|
||||
- **Command-Query Separation** — either *do* or *answer*, never both.
|
||||
- **Prefer exceptions (or Result types) to error codes.**
|
||||
- **DRY** — Don't Repeat Yourself. Duplication is the #1 smell.
|
||||
|
||||
## 3. Comments
|
||||
|
||||
> "Don't comment bad code — rewrite it." — Brian Kernighan
|
||||
|
||||
Every comment is a failure to make code self-explanatory. Before writing a comment, ask: *can I rename or extract?*
|
||||
|
||||
**Good comments (rare):**
|
||||
- Legal headers, regex explanations, wire protocol details
|
||||
- **Intent** — *why* (not *what*)
|
||||
- Warnings of consequences ("this test takes two hours")
|
||||
- TODOs (prune regularly)
|
||||
|
||||
**Delete on sight:** redundant comments, journaling (`// added by Rick`), closing-brace comments, commented-out code, mandated noise.
|
||||
|
||||
## 4. Formatting
|
||||
|
||||
- **Newspaper metaphor:** high-level first, details as you scroll.
|
||||
- **Vertical density:** related concepts close together. Caller above callee.
|
||||
- **Blank lines** separate concepts, not pad.
|
||||
- **Indentation = abstraction signal.** Ideal functions have ≤2 indentation levels.
|
||||
|
||||
## 5. Objects and Data Structures
|
||||
|
||||
- **DTOs are data structures, not objects.**
|
||||
- **Law of Demeter** — don't talk to strangers. No train wrecks (`a.getB().getC().doSomething()`).
|
||||
- **Tell, don't ask** — tell the object to do the work instead of asking for state and deciding.
|
||||
|
||||
## 6. Error Handling
|
||||
|
||||
- Use exceptions/Result types, not return codes.
|
||||
- Write try-catch-finally first when an operation can fail.
|
||||
- Wrap third-party exceptions in your own types.
|
||||
- **Don't return null.** Return empty collections or use Option/Result.
|
||||
- **Don't pass null.** Fail fast at boundaries.
|
||||
|
||||
## 7. Tests
|
||||
|
||||
- **Three Laws of TDD:** 1) no production code without a failing test, 2) no more test than sufficient to fail, 3) no more production code than sufficient to pass.
|
||||
- **F.I.R.S.T.:** Fast, Independent, Repeatable, Self-validating, Timely.
|
||||
- Test code is first-class — same quality as production code.
|
||||
|
||||
## 8. Classes
|
||||
|
||||
- **Small by responsibility**, not by lines. SRP: one reason to change, one actor.
|
||||
- **Cohesion** — methods should use most instance variables. Low cohesion = two classes in one.
|
||||
- **Organize for change** — isolate volatile concepts behind interfaces.
|
||||
|
||||
## 9. Systems
|
||||
|
||||
- **Separate construction from use** — wiring lives in one place.
|
||||
- **Dependency injection** over hardcoded `new` deep in business logic.
|
||||
- Cross-cutting concerns (logging, security, metrics) belong in middleware, not scattered code.
|
||||
|
||||
## Code Smells — Quick Checklist
|
||||
|
||||
| Category | Smells |
|
||||
|----------|--------|
|
||||
| Functions | >3 args, flag params, dead params, obscure intent, misplaced responsibility |
|
||||
| Classes | Feature envy, god class, inappropriate intimacy, lazy class |
|
||||
| General | Duplication, magic numbers, inconsistent naming, negative conditionals, switch on type |
|
||||
| Names | `data`/`info`/`handle`, not matching abstraction level, Hungarian notation |
|
||||
| Tests | Insufficient coverage, skipped tests, order-dependent, slow, over-mocking |
|
||||
@@ -0,0 +1,89 @@
|
||||
---
|
||||
name: design-patterns
|
||||
description: Guidance on Gang of Four design patterns and modern alternatives — when to use each pattern, how to implement it correctly, and when to avoid it. Use when designing class structures, solving recurring design problems, refactoring switch/if-else chains, or whenever the user mentions "design patterns," "GoF," "Factory," "Strategy," "Observer," "Singleton," "Adapter," "Decorator," or "DI."
|
||||
---
|
||||
|
||||
# Design Patterns
|
||||
|
||||
Patterns are solutions to recurring design problems. Use the right pattern for the right axis of change.
|
||||
|
||||
## When to Use Patterns
|
||||
|
||||
- **More types, fewer operations** → OO patterns (polymorphism)
|
||||
- **More operations, fewer types** → functional patterns (pattern matching, functions)
|
||||
- **Expected to change together?** Keep them together in one abstraction.
|
||||
- **Expected to change independently?** Separate them with an interface.
|
||||
|
||||
## Creational Patterns
|
||||
|
||||
| Pattern | When | Why |
|
||||
|---------|------|-----|
|
||||
| **Factory Method** | A class can't know the type of objects it must create | Push creation decision to subclasses |
|
||||
| **Abstract Factory** | Need families of related objects | Enforce compatibility across a product line |
|
||||
| **Builder** | Object construction has many optional parts | Replace telescoping constructors |
|
||||
| **Singleton** | Exactly one instance is needed | *Avoid if possible* — use dependency injection instead |
|
||||
| **Prototype** | Creating objects is expensive; cloning is cheaper | Copy-on-write, configurable app |
|
||||
|
||||
**Modern alternative:** For factories, prefer passing a function/constructor directly (`(config) => new Connection(config)`) over a factory class.
|
||||
|
||||
## Structural Patterns
|
||||
|
||||
| Pattern | When | Why |
|
||||
|---------|------|-----|
|
||||
| **Adapter** | Interface mismatch between expected and actual | Wrap, don't modify |
|
||||
| **Bridge** | Abstraction and implementation vary independently | Decouple interface from implementation |
|
||||
| **Composite** | Treat individual objects and compositions uniformly | Menu trees, file systems, UI trees |
|
||||
| **Decorator** | Add responsibilities without subclassing | Middleware, streams, logging wrappers |
|
||||
| **Facade** | Simplify a complex subsystem | Single entry point to complex system |
|
||||
| **Flyweight** | Many fine-grained objects are too expensive | Share intrinsic state across instances |
|
||||
| **Proxy** | Control access to another object | Lazy loading, caching, access control, logging |
|
||||
|
||||
**Modern alternative:** Middleware chains (e.g., Hono/Express middleware) are a functional take on Decorator. Proxy is often handled by AOP or proxy libraries.
|
||||
|
||||
## Behavioral Patterns
|
||||
|
||||
| Pattern | When | Why |
|
||||
|---------|------|-----|
|
||||
| **Strategy** | Multiple algorithms for the same task | Pass behavior as a parameter |
|
||||
| **Observer** | One-to-many dependency where state changes need notification | Event emitters, pub/sub |
|
||||
| **Command** | Parameterize operations, queue, undo, log | Transaction logging, job queues |
|
||||
| **Template Method** | Skeleton of algorithm varies in steps | Subclasses override specific steps |
|
||||
| **Iterator** | Access elements sequentially without exposing structure | Built into every modern language |
|
||||
| **State** | Object behavior changes when its state changes | State machines |
|
||||
| **Mediator** | Reduce coupling between communicating objects | Chat rooms, UI coordination |
|
||||
| **Chain of Responsibility** | Pass request along handler chain until one handles it | Middleware, validation pipelines |
|
||||
| **Visitor** | New operation on a stable object structure | AST processing, serialization |
|
||||
| **Memento** | Capture and restore internal state | Undo/redo, snapshots |
|
||||
| **Interpreter** | Grammar interpretation | DSLs, parsers |
|
||||
|
||||
**Modern alternative:** Strategy → pass a lambda/closure. Observer → use reactive streams or event emitters. Command → functions are commands.
|
||||
|
||||
## Pattern Selection Guide
|
||||
|
||||
Ask these questions:
|
||||
1. **What is changing?** Encapsulate what varies.
|
||||
2. **What's the axis of change?** More types (OO) or more operations (functional)?
|
||||
3. **Is there a simpler alternative?** A function parameter is often enough.
|
||||
4. **Does the pattern add clarity or complexity?** Patterns justify themselves only if they reduce overall complexity.
|
||||
|
||||
## Modern Patterns (Post-GoF)
|
||||
|
||||
- **Dependency Injection** — pass dependencies in, don't create them internally
|
||||
- **Repository** — abstraction over data access (not in GoF, but ubiquitous)
|
||||
- **CQRS** — separate read and write models
|
||||
- **Event Sourcing** — store events, derive state
|
||||
- **Saga** — orchestrate distributed transactions
|
||||
- **Circuit Breaker** — fail fast when downstream fails
|
||||
|
||||
## Deeper Reference
|
||||
|
||||
When the task calls for it, load:
|
||||
|
||||
- **[references/catalog.md](references/catalog.md)** — Full GoF pattern catalog with code examples, real-world usage, modern alternatives, and "when to NOT use" guidance for each pattern.
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- ❌ **Pattern for pattern's sake** — a simple function is better than a Strategy class with one implementation
|
||||
- ❌ **God Singleton** — global state disguised as a pattern
|
||||
- ❌ **AbstractFactoryFactory** — over-abstracting what should be a simple `new`
|
||||
- ❌ **The Blob** — one class/function that does everything
|
||||
@@ -0,0 +1,331 @@
|
||||
# Design Patterns Catalog
|
||||
|
||||
## Creational Patterns
|
||||
|
||||
### Factory Method
|
||||
Define an interface for creating an object, but let subclasses decide which class to instantiate.
|
||||
|
||||
```typescript
|
||||
// The pattern
|
||||
interface PaymentGateway { charge(amount: Money): Result }
|
||||
class StripeGateway implements PaymentGateway { ... }
|
||||
class MidtransGateway implements PaymentGateway { ... }
|
||||
|
||||
class PaymentFactory {
|
||||
static create(type: 'stripe' | 'midtrans'): PaymentGateway {
|
||||
if (type === 'stripe') return new StripeGateway();
|
||||
return new MidtransGateway();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**When:** A class doesn't know the exact type of objects it must create.
|
||||
**Modern alt:** `(type) => new Gateway(type)` — factory function, not a class.
|
||||
|
||||
### Abstract Factory
|
||||
Provide an interface for creating *families* of related objects.
|
||||
|
||||
```typescript
|
||||
interface UIFactory { createButton(): Button; createDialog(): Dialog }
|
||||
class MaterialFactory implements UIFactory { ... }
|
||||
class AntDesignFactory implements UIFactory { ... }
|
||||
```
|
||||
|
||||
**When:** You need to enforce that objects from the same family are used together (Material button with Material dialog).
|
||||
**Real example:** UI theme systems, database abstraction across vendors.
|
||||
|
||||
### Builder
|
||||
Separate the construction of a complex object from its representation.
|
||||
|
||||
```typescript
|
||||
class QueryBuilder {
|
||||
private select: string[] = [];
|
||||
private from = '';
|
||||
private where: string[] = [];
|
||||
|
||||
select(...fields: string[]) { this.select.push(...fields); return this; }
|
||||
from(table: string) { this.from = table; return this; }
|
||||
where(condition: string) { this.where.push(condition); return this; }
|
||||
build() { return `SELECT ${this.select.join(', ')} FROM ${this.from} WHERE ${this.where.join(' AND ')}`; }
|
||||
}
|
||||
|
||||
// Usage
|
||||
new QueryBuilder().select('id', 'name').from('users').where('active = true').build();
|
||||
```
|
||||
|
||||
**When:** An object has many optional parts or complex construction.
|
||||
|
||||
### Singleton
|
||||
Ensure a class has only one instance and provide a global access point.
|
||||
|
||||
```typescript
|
||||
// The pattern — but prefer DI
|
||||
class Config {
|
||||
private static instance: Config;
|
||||
static getInstance(): Config {
|
||||
if (!Config.instance) Config.instance = new Config();
|
||||
return Config.instance;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**⚠️ Use sparingly.** Singletons are global state in disguise. Prefer DI container managing a single instance.
|
||||
**Only acceptable:** Logging, hardware interfaces, actual system-wide singletons.
|
||||
|
||||
### Prototype
|
||||
Create new objects by cloning an existing instance.
|
||||
|
||||
```typescript
|
||||
class Document implements Cloneable {
|
||||
clone(): Document { return structuredClone(this); }
|
||||
}
|
||||
```
|
||||
|
||||
**When:** Creating objects is expensive and cloning is cheaper (config objects, document templates).
|
||||
|
||||
---
|
||||
|
||||
## Structural Patterns
|
||||
|
||||
### Adapter
|
||||
Convert one interface into another that the client expects.
|
||||
|
||||
```typescript
|
||||
// Legacy interface
|
||||
class LegacyMailer { sendMail(from: string, to: string, body: string) { ... } }
|
||||
// New interface
|
||||
interface NotificationService { send(recipient: string, message: string): void }
|
||||
|
||||
class MailerAdapter implements NotificationService {
|
||||
constructor(private legacy: LegacyMailer) {}
|
||||
send(recipient: string, message: string) {
|
||||
this.legacy.sendMail('noreply@x.com', recipient, message);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**When:** Interface mismatch between expected and actual. Wrap, don't modify.
|
||||
|
||||
### Bridge
|
||||
Decouple an abstraction from its implementation so they can vary independently.
|
||||
|
||||
```typescript
|
||||
// Abstraction
|
||||
abstract class Remote { abstract press(): void }
|
||||
class TVRemote extends Remote { constructor(private device: TV) {} }
|
||||
class RadioRemote extends Remote { constructor(private device: Radio) {} }
|
||||
|
||||
// Implementation
|
||||
interface Device { on(): void; off(): void }
|
||||
class SonyTV implements Device { ... }
|
||||
class PhilipsTV implements Device { ... }
|
||||
```
|
||||
|
||||
**When:** Both the abstraction and implementation vary independently.
|
||||
|
||||
### Composite
|
||||
Compose objects into tree structures to represent part-whole hierarchies.
|
||||
|
||||
```typescript
|
||||
interface FileSystemNode { getSize(): number; }
|
||||
class File implements FileSystemNode { constructor(private size: number) {} getSize() { return this.size; } }
|
||||
class Directory implements FileSystemNode {
|
||||
constructor(private children: FileSystemNode[]) {}
|
||||
getSize() { return this.children.reduce((acc, c) => acc + c.getSize(), 0); }
|
||||
}
|
||||
```
|
||||
|
||||
**When:** Tree structures (UI trees, file systems, menu hierarchies).
|
||||
|
||||
### Decorator
|
||||
Dynamically add responsibilities to an object.
|
||||
|
||||
```typescript
|
||||
interface DataSource { write(data: string): void; read(): string }
|
||||
class FileDataSource implements DataSource { ... }
|
||||
|
||||
class CompressionDecorator implements DataSource {
|
||||
constructor(private wrappee: DataSource) {}
|
||||
write(data: string) { this.wrappee.write(compress(data)); }
|
||||
read() { return decompress(this.wrappee.read()); }
|
||||
}
|
||||
|
||||
new CompressionDecorator(new EncryptionDecorator(new FileDataSource('file.txt')));
|
||||
```
|
||||
|
||||
**When:** You need to layer behavior (middleware, streams, I/O pipelines).
|
||||
**Modern alt:** Hono/Express middleware chains are a functional take on Decorator.
|
||||
|
||||
### Facade
|
||||
Provide a unified interface to a complex subsystem.
|
||||
|
||||
```typescript
|
||||
class PaymentFacade {
|
||||
async pay(amount: Money, method: PaymentMethod): Promise<Receipt> {
|
||||
const gateway = PaymentFactory.create(method.type);
|
||||
const result = await gateway.charge(amount);
|
||||
await this.receiptRepo.save(result.receipt);
|
||||
await this.notifier.send(result.receipt);
|
||||
return result.receipt;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**When:** Simplifying a complex subsystem with a single entry point.
|
||||
|
||||
### Flyweight
|
||||
Share common state across many objects to save memory.
|
||||
|
||||
```typescript
|
||||
class Character {
|
||||
constructor(public char: string, public style: TextStyle) {} // style shared
|
||||
}
|
||||
class TextStyle { constructor(public font: string, public size: number, public bold: boolean) {} }
|
||||
```
|
||||
|
||||
**When:** Many fine-grained objects with shared intrinsic state (text editors, game particle systems).
|
||||
|
||||
### Proxy
|
||||
Control access to another object.
|
||||
|
||||
```typescript
|
||||
class CachedUserRepo implements UserRepository {
|
||||
private cache = new Map<string, User>();
|
||||
constructor(private real: UserRepository) {}
|
||||
async findById(id: string): Promise<User | null> {
|
||||
if (this.cache.has(id)) return this.cache.get(id)!;
|
||||
const user = await this.real.findById(id);
|
||||
if (user) this.cache.set(id, user);
|
||||
return user;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**When:** Lazy loading, caching, access control, logging — without modifying the real object.
|
||||
|
||||
---
|
||||
|
||||
## Behavioral Patterns
|
||||
|
||||
### Strategy
|
||||
Define a family of algorithms, encapsulate each one, and make them interchangeable.
|
||||
|
||||
```typescript
|
||||
interface AuthStrategy { authenticate(creds: Credentials): Promise<User> }
|
||||
class PasswordAuth implements AuthStrategy { ... }
|
||||
class OAuthStrategy implements AuthStrategy { ... }
|
||||
class MFAStrategy implements AuthStrategy { ... }
|
||||
|
||||
class AuthService {
|
||||
constructor(private strategy: AuthStrategy) {}
|
||||
async login(creds: Credentials) { return this.strategy.authenticate(creds); }
|
||||
}
|
||||
```
|
||||
|
||||
**When:** Multiple algorithms for the same task. **Modern alt:** Pass a lambda/function.
|
||||
|
||||
### Observer
|
||||
Define a one-to-many dependency so that when one object changes state, all dependents are notified.
|
||||
|
||||
```typescript
|
||||
class EventBus {
|
||||
private handlers = new Map<string, Function[]>();
|
||||
on(event: string, handler: Function) { ... }
|
||||
emit(event: string, data: unknown) { ... }
|
||||
}
|
||||
```
|
||||
|
||||
**When:** Event handling, pub/sub, data binding. **Modern alt:** Reactive streams (RxJS), event emitters.
|
||||
|
||||
### Command
|
||||
Encapsulate a request as an object, allowing parameterization, queuing, and undo.
|
||||
|
||||
```typescript
|
||||
interface Command { execute(): void; undo(): void }
|
||||
class CreateOrderCommand implements Command { ... }
|
||||
class CancelOrderCommand implements Command { ... }
|
||||
class CommandQueue {
|
||||
private history: Command[] = [];
|
||||
execute(cmd: Command) { cmd.execute(); this.history.push(cmd); }
|
||||
undo() { this.history.pop()?.undo(); }
|
||||
}
|
||||
```
|
||||
|
||||
**When:** Job queues, undo/redo, transaction logging.
|
||||
**Modern alt:** Functions as first-class commands (JS/TS closures).
|
||||
|
||||
### Template Method
|
||||
Define the skeleton of an algorithm, letting subclasses override specific steps.
|
||||
|
||||
```typescript
|
||||
abstract class DataExporter {
|
||||
export(): string {
|
||||
const data = this.fetch();
|
||||
const formatted = this.format(data);
|
||||
return this.write(formatted);
|
||||
}
|
||||
abstract fetch(): unknown;
|
||||
abstract format(data: unknown): string;
|
||||
abstract write(data: string): string;
|
||||
}
|
||||
class CSVExporter extends DataExporter { ... }
|
||||
class JSONExporter extends DataExporter { ... }
|
||||
```
|
||||
|
||||
**When:** Multiple implementations share the same algorithm structure but vary in steps.
|
||||
|
||||
### State
|
||||
Allow an object to alter its behavior when its internal state changes.
|
||||
|
||||
```typescript
|
||||
interface OrderState { next(order: Order): void; cancel(order: Order): void }
|
||||
class PendingState implements OrderState { ... }
|
||||
class PaidState implements OrderState { ... }
|
||||
class ShippedState implements OrderState { ... }
|
||||
class CancelledState implements OrderState { ... }
|
||||
```
|
||||
|
||||
**When:** An object's behavior depends on its state and must change at runtime (orders, documents, workflows).
|
||||
|
||||
### Chain of Responsibility
|
||||
Pass a request along a chain of handlers until one processes it.
|
||||
|
||||
```typescript
|
||||
abstract class Handler {
|
||||
constructor(protected next?: Handler) {}
|
||||
abstract handle(request: HttpRequest): HttpResponse | null;
|
||||
}
|
||||
|
||||
class AuthHandler extends Handler { handle(r) { return r.isAuthenticated ? this.next?.handle(r) : new HttpResponse(401); } }
|
||||
class RateLimitHandler extends Handler { handle(r) { return r.notRateLimited ? this.next?.handle(r) : new HttpResponse(429); } }
|
||||
class Router extends Handler { handle(r) { return new HttpResponse(200, 'OK'); } }
|
||||
|
||||
new AuthHandler(new RateLimitHandler(new Router()));
|
||||
```
|
||||
|
||||
**When:** Middleware, validation pipelines, logging chains.
|
||||
|
||||
### Visitor
|
||||
Represent an operation to be performed on elements of an object structure.
|
||||
|
||||
```typescript
|
||||
interface ASTNode { accept(v: Visitor): void }
|
||||
class NumberNode implements ASTNode { accept(v) { v.visitNumber(this); } }
|
||||
class BinaryOpNode implements ASTNode { accept(v) { v.visitBinaryOp(this); } }
|
||||
|
||||
interface Visitor { visitNumber(node: NumberNode): void; visitBinaryOp(node: BinaryOpNode): void }
|
||||
class Evaluator implements Visitor { ... }
|
||||
class ASTPrinter implements Visitor { ... }
|
||||
```
|
||||
|
||||
**When:** You need to add new operations to a stable object structure. Use with caution — it violates OCP if the structure changes.
|
||||
|
||||
---
|
||||
|
||||
## When to NOT Use a Pattern
|
||||
|
||||
- **Pattern for pattern's sake** — a simple function is better than a Strategy class with one implementation.
|
||||
- **AbstractFactoryFactory** — over-abstracting what a simple `new` handles.
|
||||
- **Singleton as global state** — use DI with single instance.
|
||||
- **Observer with no actual observers** — simple callbacks suffice.
|
||||
- **Visitor with a changing object structure** — every new type means updating all visitors.
|
||||
@@ -0,0 +1,144 @@
|
||||
---
|
||||
name: docker
|
||||
description: Docker best practices — Dockerfile optimization, multi-stage builds, Docker Compose, networking, security, and image management. Use when writing Dockerfiles, designing container infrastructure, debugging docker issues, or whenever the user mentions "Docker," "Dockerfile," "docker-compose," "compose," "multi-stage," "container," "image," "registry," or "OCI."
|
||||
---
|
||||
|
||||
# Docker Best Practices
|
||||
|
||||
## Dockerfile Best Practices
|
||||
|
||||
### Multi-Stage Builds
|
||||
|
||||
```dockerfile
|
||||
# Stage 1: Build
|
||||
FROM node:22-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY package.json bun.lock ./
|
||||
RUN bun install --frozen-lockfile
|
||||
COPY . .
|
||||
RUN bun run build
|
||||
|
||||
# Stage 2: Production
|
||||
FROM node:22-alpine AS runner
|
||||
WORKDIR /app
|
||||
RUN addgroup --system app && adduser --system app
|
||||
COPY --from=builder /app/dist ./dist
|
||||
COPY --from=builder /app/node_modules ./node_modules
|
||||
COPY --from=builder /app/package.json ./
|
||||
USER app
|
||||
EXPOSE 3000
|
||||
CMD ["node", "dist/index.js"]
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
- **Minimal base image** — Alpine or `scratch` for binaries, `distroless` for runtimes.
|
||||
- **Combine RUN commands** — each `RUN` creates a layer. Chain with `&&`.
|
||||
- **Use `.dockerignore`** — exclude `node_modules`, `.git`, `*.md`, CI files.
|
||||
- **Don't run as root** — `USER app` (not `root`).
|
||||
- **Prefer COPY over ADD** — ADD has magic behavior (tar extraction, URL fetch).
|
||||
- **Leverage build cache** — order `COPY` from least to most frequently changed.
|
||||
|
||||
### Optimize Layer Caching
|
||||
```dockerfile
|
||||
# 1. Install deps first (changes rarely)
|
||||
COPY package.json bun.lock ./
|
||||
RUN bun install --frozen-lockfile
|
||||
|
||||
# 2. Copy source (changes often — last)
|
||||
COPY . .
|
||||
RUN bun run build
|
||||
```
|
||||
|
||||
## Docker Compose
|
||||
|
||||
```yaml
|
||||
# Always join shared network
|
||||
services:
|
||||
app:
|
||||
container_name: my-service
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
networks:
|
||||
- app-shared-net
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--spider", "http://localhost:3000/health"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
networks:
|
||||
app-shared-net:
|
||||
external: true
|
||||
```
|
||||
|
||||
### Typical Services Layout (this repo)
|
||||
- **Redis:** `shared.yml` — always first
|
||||
- **NATS:** `nats.yml` — JetStream-enabled
|
||||
- **Dapr:** `dapr.yml` — placement service
|
||||
- **Traefik:** `traefik.yml` — reverse proxy
|
||||
- **App + Sidecar:** `app.yml` — service + daprd sidecar
|
||||
|
||||
## Security
|
||||
|
||||
- **Don't run as root** — always `USER app` with least privileges.
|
||||
- **Read-only root** — `--read-only` flag. Mount tmpfs for writable dirs.
|
||||
- **No secrets in images** — use build args only for non-sensitive values. Secrets via env.
|
||||
- **Image scanning** — `docker scout` or Trivy for CVE scanning.
|
||||
- **Drop capabilities** — `--cap-drop=ALL --cap-add=NET_BIND_SERVICE` in compose.
|
||||
- **Healthchecks** — prevent routing to dead containers.
|
||||
|
||||
## Image Tagging
|
||||
|
||||
```
|
||||
# Pattern
|
||||
sha-<short-sha> # Immutable — for deterministic rollbacks
|
||||
latest # Mutable — convenience
|
||||
|
||||
# Example (from this repo's CI)
|
||||
ghcr.io/asepharyana/asepharyana-hub/<service>:sha-a1b2c3d
|
||||
ghcr.io/asepharyana/asepharyana-hub/<service>:latest
|
||||
```
|
||||
|
||||
## Networking
|
||||
|
||||
- **All containers** join `app-shared-net` (external Docker bridge).
|
||||
- **DNS resolution** via Docker DNS (container name = hostname).
|
||||
- **Cross-VPS** via Tailscale (`100.64.0.0/10`).
|
||||
- **Expose only needed ports** — Traefik handles external traffic on port 443.
|
||||
|
||||
## Debugging
|
||||
|
||||
```bash
|
||||
# Inspect layers
|
||||
docker history <image>
|
||||
|
||||
# Check image size
|
||||
docker images <image>
|
||||
|
||||
# Check running container
|
||||
docker inspect <container>
|
||||
|
||||
# Shell into container
|
||||
docker exec -it <container> sh
|
||||
|
||||
# Check container logs
|
||||
docker logs <container>
|
||||
|
||||
# Analyze build cache
|
||||
docker build --no-cache-filter=production .
|
||||
```
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- ❌ Running as root — security risk
|
||||
- ❌ `latest` tag in production — use immutable SHA tags
|
||||
- ❌ Large images — Alpine/multi-stage keeps them small
|
||||
- ❌ Multiple services per container — one process per container
|
||||
- ❌ Installing build tools in production image — use multi-stage
|
||||
- ❌ Hardcoded secrets in Dockerfile — use env vars or secrets mount
|
||||
- ❌ No `.dockerignore` — sends entire project context to daemon
|
||||
@@ -0,0 +1,165 @@
|
||||
---
|
||||
name: documentation
|
||||
description: Best practices for software documentation — README, API docs, ADRs, inline comments, changelogs, and knowledge base organization. Use when writing README files, designing documentation strategy, adding inline comments, or whenever the user mentions "documentation," "README," "ADR," "changelog," "docstring," "wiki," "documentation as code," or "docs."
|
||||
---
|
||||
|
||||
# Documentation Best Practices
|
||||
|
||||
## The Minimalist Philosophy
|
||||
|
||||
> "Produce no document unless its need is immediate and significant." — Robert C. Martin
|
||||
|
||||
Documentation has ongoing cost: maintenance, outdated content, reader trust erosion. Write less, maintain ruthlessly.
|
||||
|
||||
**Rule:** If a document would be wrong within 6 months, don't write it — automate it or make the code self-explanatory.
|
||||
|
||||
## Documentation Types (by audience)
|
||||
|
||||
### 1. README — for newcomers
|
||||
Every project needs one. Answers 4 questions in order:
|
||||
1. **What is this?** — one-paragraph description
|
||||
2. **Why does it exist?** — what problem it solves
|
||||
3. **How do I run it?** — quickstart: install → configure → run
|
||||
4. **Where do I go for help?** — link to issues, docs, chat
|
||||
|
||||
```markdown
|
||||
# project-name
|
||||
Brief description (1-2 sentences).
|
||||
|
||||
## Quickstart
|
||||
```bash
|
||||
npm install
|
||||
cp .env.example .env
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## Configuration
|
||||
Key environment variables, config files.
|
||||
|
||||
## API
|
||||
Link to OpenAPI spec or API docs.
|
||||
|
||||
## Development
|
||||
Testing, linting, building, contributing guide.
|
||||
```
|
||||
|
||||
**README anti-patterns:**
|
||||
- ❌ Outdated setup steps (worse than no setup guide)
|
||||
- ❌ Long architecture essays (put in ADR or docs/)
|
||||
- ❌ Contributor lists (git log handles this)
|
||||
- ❌ Badges from tools you don't use
|
||||
|
||||
### 2. ADRs (Architecture Decision Records) — for maintainers
|
||||
|
||||
Record *why* a decision was made, not *what* was decided (that's in the code).
|
||||
|
||||
```
|
||||
docs/adr/
|
||||
├── 001-use-postgres-for-primary-store.md
|
||||
├── 002-use-dapr-for-pub-sub.md
|
||||
└── 003-migrate-to-biome-from-eslint.md
|
||||
```
|
||||
|
||||
**Template:**
|
||||
```markdown
|
||||
# ADR-001: Use PostgreSQL for Primary Store
|
||||
|
||||
**Date:** 2024-01-15
|
||||
**Status:** Accepted | Proposed | Deprecated | Superseded
|
||||
|
||||
## Context
|
||||
Why this decision was needed, what alternatives were considered.
|
||||
|
||||
## Decision
|
||||
What was decided and why over alternatives.
|
||||
|
||||
## Consequences
|
||||
What becomes easier, harder, or needs migration.
|
||||
```
|
||||
|
||||
### 3. API Documentation — for consumers
|
||||
|
||||
- **REST:** OpenAPI 3.x spec. Generate from code (Hono Zod OpenAPI, FastAPI Swagger).
|
||||
- **GraphQL:** Schema is documentation — auto-generated from SDL.
|
||||
- **Libraries:** API reference (JSDoc, rustdoc, godoc, pydoc).
|
||||
- **Include:** endpoint/method, params, request/response schema, errors, example, auth.
|
||||
|
||||
### 4. Inline Comments — for future developers
|
||||
|
||||
**Good comments (rare but valuable):**
|
||||
```typescript
|
||||
// WHY: This ordering ensures we process the oldest items first
|
||||
// so failed retries don't starve newer entries. Priorities > 5
|
||||
// are reserved for system-internal events.
|
||||
```
|
||||
|
||||
**Bad comments (delete on sight):**
|
||||
```typescript
|
||||
// ❌ Redundant
|
||||
i++ // increment i
|
||||
|
||||
// ❌ Misleading (out of date)
|
||||
// This function validates input (it no longer does)
|
||||
|
||||
// ❌ Mumbling
|
||||
// handle the thing
|
||||
|
||||
// ❌ Journal
|
||||
// 2024-01-15: fixed the bug
|
||||
|
||||
// ❌ Commented-out code
|
||||
// const old = calcTotal(items);
|
||||
```
|
||||
|
||||
### 5. CHANGELOG.md — for users
|
||||
|
||||
Auto-generated from commits (release-please, changie, git-cliff). Never manual.
|
||||
|
||||
```markdown
|
||||
# Changelog
|
||||
|
||||
## [1.2.0] - 2025-06-15
|
||||
### Added
|
||||
- feat(auth): Google OAuth sign-in
|
||||
- feat(ui): dark mode toggle
|
||||
|
||||
### Fixed
|
||||
- fix(billing): handle null currency in invoice generation
|
||||
- fix(api): rate-limit headers on error responses
|
||||
|
||||
### Changed
|
||||
- chore(deps): update TypeScript to 5.5
|
||||
```
|
||||
|
||||
### 6. How-to Guides — for specific tasks
|
||||
|
||||
- Focused, task-oriented. One guide = one task.
|
||||
- "How to add a new service" not "architecture overview."
|
||||
- Keep in `docs/` directory alongside the code.
|
||||
|
||||
## Architecture (for docs/)
|
||||
|
||||
```
|
||||
docs/
|
||||
├── add-new-app.md # How-to guide
|
||||
├── deployment.md # Deployment guide
|
||||
├── adr/ # Architecture Decision Records
|
||||
├── diagrams/ # Architecture diagrams (keep simple)
|
||||
└── runbooks/ # Incident response procedures
|
||||
```
|
||||
|
||||
## Automation
|
||||
|
||||
- **Pre-commit check** — warn if README has no quickstart.
|
||||
- **CI check** — verify ADR links are valid.
|
||||
- **OpenAPI validation** — CI validates spec file is up to date.
|
||||
- **Dependabot/Renovate** — keeps dependency docs fresh automatically.
|
||||
|
||||
## Documentation Anti-patterns
|
||||
|
||||
- ❌ **Rotting docs** — outdated docs are worse than no docs. Delete or update.
|
||||
- ❌ **Copy-paste docs** — duplicated content across files. Cross-reference instead.
|
||||
- ❌ **Epic README** — README that tries to document everything. Split into `docs/`.
|
||||
- ❌ **Documenting the obvious** — `// This function saves a user`
|
||||
- ❌ **No code examples** — abstract docs without concrete usage are useless.
|
||||
- ❌ **No tone** — documentation can be clear without being dry. A little personality helps.
|
||||
@@ -0,0 +1,197 @@
|
||||
---
|
||||
name: drizzle-database
|
||||
description: Drizzle ORM best practices — schema design, queries, migrations, relations, and performance. Use when designing database schemas, writing Drizzle queries, managing migrations, or whenever the user mentions "Drizzle," "Drizzle ORM," "drizzle-orm," "drizzle-kit," "schema," "migration," "PostgreSQL," "SQLite," or "database design."
|
||||
---
|
||||
|
||||
# Drizzle ORM Best Practices
|
||||
|
||||
## Schema Design
|
||||
|
||||
```typescript
|
||||
import { pgTable, serial, text, timestamp, boolean, integer, jsonb } from 'drizzle-orm/pg-core';
|
||||
import { relations } from 'drizzle-orm';
|
||||
|
||||
// Tables with explicit foreign keys
|
||||
export const users = pgTable('users', {
|
||||
id: serial('id').primaryKey(),
|
||||
email: text('email').notNull().unique(),
|
||||
name: text('name'),
|
||||
role: text('role', { enum: ['admin', 'user'] }).default('user').notNull(),
|
||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
||||
});
|
||||
|
||||
export const orders = pgTable('orders', {
|
||||
id: serial('id').primaryKey(),
|
||||
userId: integer('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
|
||||
status: text('status', { enum: ['pending', 'paid', 'shipped'] }).default('pending').notNull(),
|
||||
total: integer('total').notNull(), // in cents
|
||||
metadata: jsonb('metadata'),
|
||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||
});
|
||||
|
||||
// Relations
|
||||
export const usersRelations = relations(users, ({ many }) => ({
|
||||
orders: many(orders),
|
||||
}));
|
||||
|
||||
export const ordersRelations = relations(orders, ({ one }) => ({
|
||||
user: one(users, { fields: [orders.userId], references: [users.id] }),
|
||||
}));
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
- `serial` for auto-increment PKs, `uuid` for distributed/public IDs.
|
||||
- `timestamp` with `defaultNow()` for created/updated.
|
||||
- JSONB for flexible metadata (Postgres). Text JSON for SQLite.
|
||||
- Enums as `text` with `enum` constraint (not Postgres `CREATE TYPE` — easier migrations).
|
||||
|
||||
## Queries
|
||||
|
||||
### Basic CRUD
|
||||
```typescript
|
||||
import { eq, and, or, like, gte, lte, asc, desc, sql, inArray } from 'drizzle-orm';
|
||||
|
||||
// Create
|
||||
const [user] = await db.insert(users).values({ email: 'a@b.com' }).returning();
|
||||
|
||||
// Read
|
||||
const allUsers = await db.select().from(users);
|
||||
const user = await db.select().from(users).where(eq(users.id, id)).limit(1);
|
||||
const admins = await db.select().from(users).where(eq(users.role, 'admin')).orderBy(desc(users.createdAt));
|
||||
|
||||
// Update
|
||||
const [updated] = await db.update(users).set({ name }).where(eq(users.id, id)).returning();
|
||||
|
||||
// Delete
|
||||
await db.delete(users).where(eq(users.id, id));
|
||||
```
|
||||
|
||||
### Joins
|
||||
```typescript
|
||||
// One-to-many
|
||||
const result = await db.select()
|
||||
.from(users)
|
||||
.leftJoin(orders, eq(users.id, orders.userId))
|
||||
.where(eq(users.id, id));
|
||||
|
||||
// With relations (prepared — uses multiple queries or JOINs internally)
|
||||
const userWithOrders = await db.query.users.findFirst({
|
||||
where: eq(users.id, id),
|
||||
with: { orders: { limit: 5 } },
|
||||
});
|
||||
```
|
||||
|
||||
### Aggregations
|
||||
```typescript
|
||||
import { count, sum, avg, min, max, sql } from 'drizzle-orm';
|
||||
|
||||
const stats = await db.select({
|
||||
total: count(),
|
||||
totalRevenue: sum(orders.total),
|
||||
avgOrderValue: avg(orders.total),
|
||||
byStatus: sql`${orders.status}::text`,
|
||||
}).from(orders)
|
||||
.groupBy(orders.status);
|
||||
```
|
||||
|
||||
## Migrations (drizzle-kit)
|
||||
|
||||
```jsonc
|
||||
// drizzle.config.ts
|
||||
import { defineConfig } from 'drizzle-kit';
|
||||
|
||||
export default defineConfig({
|
||||
dialect: 'postgresql',
|
||||
schema: './src/db/schema/*.ts',
|
||||
out: './src/db/migrations',
|
||||
dbCredentials: { url: process.env.DATABASE_URL! },
|
||||
});
|
||||
```
|
||||
|
||||
```bash
|
||||
# Commands
|
||||
bunx drizzle-kit generate # Generate migration from schema changes
|
||||
bunx drizzle-kit migrate # Apply migrations to database
|
||||
bunx drizzle-kit push # Push schema (dev only — no migration files)
|
||||
bunx drizzle-kit studio # Drizzle Studio (GUI for DB inspection)
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
- Generate migrations, then apply. Use `drizzle-kit push` only in dev.
|
||||
- Code review migration files before applying to production.
|
||||
- Write custom SQL for complex migrations (backfills, data transformations).
|
||||
- Never edit generated migration files manually (unless you know what you're doing).
|
||||
|
||||
## Performance
|
||||
|
||||
### Indexes
|
||||
```typescript
|
||||
import { index, uniqueIndex } from 'drizzle-orm/pg-core';
|
||||
|
||||
export const users = pgTable('users', {
|
||||
id: serial('id').primaryKey(),
|
||||
email: text('email').notNull().unique(),
|
||||
// ...
|
||||
}, (table) => ({
|
||||
emailIdx: uniqueIndex('users_email_idx').on(table.email),
|
||||
roleIdx: index('users_role_idx').on(table.role),
|
||||
// Composite index for common queries
|
||||
createdRoleIdx: index('users_created_role_idx').on(table.createdAt, table.role),
|
||||
}));
|
||||
```
|
||||
|
||||
### N+1 Prevention
|
||||
```typescript
|
||||
// ❌ N+1 — one query per order item
|
||||
for (const order of orders) {
|
||||
const items = await db.select().from(orderItems).where(eq(orderItems.orderId, order.id));
|
||||
}
|
||||
|
||||
// ✅ Eager with `IN`
|
||||
const orderIds = orders.map(o => o.id);
|
||||
const allItems = await db.select().from(orderItems).where(inArray(orderItems.orderId, orderIds));
|
||||
```
|
||||
|
||||
### Prepared Statements
|
||||
```typescript
|
||||
const findUserByEmail = db.select().from(users).where(eq(users.email, sql.placeholder('email'))).prepare();
|
||||
|
||||
// Reuse
|
||||
const user1 = await findUserByEmail.execute({ email: 'a@b.com' });
|
||||
const user2 = await findUserByEmail.execute({ email: 'c@d.com' });
|
||||
```
|
||||
|
||||
## Repository Pattern
|
||||
|
||||
```typescript
|
||||
// Always accessed through repository — never direct db calls from routes
|
||||
export class UserRepository {
|
||||
constructor(private db: DB) {}
|
||||
|
||||
async findByEmail(email: string): Promise<User | null> {
|
||||
const result = await this.db.select().from(users).where(eq(users.email, email)).limit(1);
|
||||
return result[0] ?? null;
|
||||
}
|
||||
|
||||
async create(input: CreateUserInput): Promise<User> {
|
||||
const [user] = await this.db.insert(users).values(input).returning();
|
||||
return user;
|
||||
}
|
||||
|
||||
async update(id: number, data: Partial<User>): Promise<User | null> {
|
||||
const [user] = await this.db.update(users).set({ ...data, updatedAt: new Date() }).where(eq(users.id, id)).returning();
|
||||
return user ?? null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- ❌ Raw SQL strings instead of Drizzle query builder when Drizzle provides it
|
||||
- ❌ `select *` in production — name specific columns
|
||||
- ❌ No repository layer — Drizzle queries in route handlers
|
||||
- ❌ Missing indexes on foreign keys and filtered columns
|
||||
- ❌ `await` in loops for sequential queries — use `Promise.all` or `IN` queries
|
||||
- ❌ Editing generated migration files
|
||||
- ❌ Using `serial` for user-facing IDs — use `uuid` for public IDs
|
||||
@@ -0,0 +1,192 @@
|
||||
---
|
||||
name: elysiajs
|
||||
description: ElysiaJS (Bun) best practices — Eden Treaty, plugins, type-safe routes, Elysia validation, and middleware. Use when building ElysiaJS backend APIs, or whenever the user mentions "Elysia," "ElysiaJS," "Eden," "Eden Treaty," "Bun," "Elysia plugin," or "Elysia validation."
|
||||
---
|
||||
|
||||
# ElysiaJS Best Practices
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── modules/ # Feature modules
|
||||
│ ├── users/
|
||||
│ │ ├── routes.ts # Elysia routes (thin)
|
||||
│ │ ├── service.ts # Business logic
|
||||
│ │ └── repository.ts # Data access
|
||||
│ └── orders/
|
||||
├── plugins/ # Custom Elysia plugins
|
||||
├── lib/ # Shared utilities
|
||||
├── db/ # Database schema, migrations
|
||||
└── index.ts # App entry point
|
||||
```
|
||||
|
||||
## Route Definition (Type-Safe)
|
||||
|
||||
```typescript
|
||||
import { Elysia, t } from 'elysia';
|
||||
import { userService } from './service';
|
||||
|
||||
const users = new Elysia({ prefix: '/users' })
|
||||
.model({
|
||||
'user.create': t.Object({
|
||||
email: t.String({ format: 'email' }),
|
||||
name: t.Optional(t.String({ minLength: 1 })),
|
||||
}),
|
||||
'user.response': t.Object({
|
||||
id: t.String(),
|
||||
email: t.String(),
|
||||
name: t.Optional(t.String()),
|
||||
}),
|
||||
})
|
||||
.get('/', async ({ query }) => {
|
||||
const result = await userService.list(query);
|
||||
return result;
|
||||
}, {
|
||||
query: t.Object({
|
||||
page: t.Optional(t.Numeric({ minimum: 1 })),
|
||||
limit: t.Optional(t.Numeric({ minimum: 1, maximum: 100 })),
|
||||
}),
|
||||
response: t.Array(t.Ref('user.response')),
|
||||
})
|
||||
.post('/', async ({ body }) => {
|
||||
const user = await userService.create(body);
|
||||
return user;
|
||||
}, {
|
||||
body: t.Ref('user.create'),
|
||||
response: t.Ref('user.response'),
|
||||
detail: { summary: 'Create user', tags: ['Users'] },
|
||||
});
|
||||
|
||||
export { users };
|
||||
```
|
||||
|
||||
## Eden Treaty (Full-Stack Type Safety)
|
||||
|
||||
```typescript
|
||||
// Server (route definition inline above creates Eden types automatically)
|
||||
|
||||
// Client — automatically typed
|
||||
import { treaty } from '@elysiajs/eden';
|
||||
import type { App } from '../server';
|
||||
|
||||
const api = treaty<App>('http://localhost:3000');
|
||||
|
||||
// Fully typed — autocomplete for paths, params, response
|
||||
const { data, error } = await api.users.index.get({ query: { page: 1, limit: 20 } });
|
||||
// data is typed as UserResponse[]
|
||||
```
|
||||
|
||||
## Plugins Pattern
|
||||
|
||||
```typescript
|
||||
// Custom plugin — encapsulate cross-cutting concerns
|
||||
import { Elysia } from 'elysia';
|
||||
|
||||
const authPlugin = (app: Elysia) =>
|
||||
app
|
||||
.decorate('auth', new AuthService())
|
||||
.derive(({ headers, auth }) => {
|
||||
const token = headers.authorization?.split(' ')[1];
|
||||
const user = token ? auth.verify(token) : null;
|
||||
return { user };
|
||||
})
|
||||
.onError(({ code, error }) => {
|
||||
if (code === 'VALIDATION') return { error: error.message };
|
||||
});
|
||||
|
||||
// Apply to app
|
||||
const app = new Elysia()
|
||||
.use(authPlugin)
|
||||
.use(cors())
|
||||
.use(swagger())
|
||||
.group('/api/v1', (app) => app.use(users))
|
||||
.listen(3000);
|
||||
```
|
||||
|
||||
## Validation
|
||||
|
||||
```typescript
|
||||
import { t } from 'elysia';
|
||||
|
||||
// Reusable models
|
||||
const PaginationModel = t.Object({
|
||||
page: t.Numeric({ minimum: 1, default: 1 }),
|
||||
limit: t.Numeric({ minimum: 1, maximum: 100, default: 20 }),
|
||||
});
|
||||
|
||||
const ErrorModel = t.Object({
|
||||
error: t.String(),
|
||||
details: t.Optional(t.Array(t.Object({
|
||||
field: t.String(),
|
||||
message: t.String(),
|
||||
}))),
|
||||
});
|
||||
|
||||
// Use `model()` to share across routes
|
||||
const app = new Elysia()
|
||||
.model({
|
||||
pagination: PaginationModel,
|
||||
error: ErrorModel,
|
||||
});
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
```typescript
|
||||
import { Elysia, NotFoundError, ValidationError } from 'elysia';
|
||||
|
||||
const app = new Elysia()
|
||||
.onError(({ code, error, set }) => {
|
||||
switch (code) {
|
||||
case 'NOT_FOUND':
|
||||
set.status = 404;
|
||||
return { error: 'Resource not found' };
|
||||
case 'VALIDATION':
|
||||
set.status = 422;
|
||||
return { error: error.message };
|
||||
default:
|
||||
set.status = 500;
|
||||
console.error(error);
|
||||
return { error: 'Internal server error' };
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Performance
|
||||
|
||||
- **Elysia runs on Bun** — Bun is fast. No need for extra micro-optimizations initially.
|
||||
- **Use `scoped: true`** for per-request state isolation.
|
||||
- **Static routes** — use `staticPlugin` for serving files.
|
||||
- **WebSocket** — built-in WS support, no extra lib needed.
|
||||
|
||||
## Testing
|
||||
|
||||
```typescript
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import { Elysia } from 'elysia';
|
||||
import { userRoutes } from './routes';
|
||||
|
||||
const app = new Elysia().use(userRoutes);
|
||||
|
||||
describe('users', () => {
|
||||
it('returns 422 for invalid email', async () => {
|
||||
const res = await app
|
||||
.handle(new Request('http://localhost/users', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email: 'not-an-email' }),
|
||||
}));
|
||||
expect(res.status).toBe(422);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- ❌ Business logic in route handlers — extract to service layer
|
||||
- ❌ No validation on inputs — every route must have a schema
|
||||
- ❌ Mixing Elysia/Express patterns — Elysia is not Express
|
||||
- ❌ `any` types — Elysia's superpower is type-safety
|
||||
- ❌ Global state in plugins — use decorator/derive for per-request state
|
||||
- ❌ Using `t.Any()` — defeats validation
|
||||
@@ -0,0 +1,201 @@
|
||||
---
|
||||
name: engineering-principles
|
||||
description: Foundational software engineering principles that apply across all languages, frameworks, and project types — correctness, simplicity, YAGNI, KISS, DRY, root-cause fixes, least astonishment, explicit over implicit, fail fast, and professional craftsmanship. This skill is a baseline: apply these principles to every code decision, review, and architecture discussion, regardless of language or framework. Engage proactively whenever writing, reviewing, or designing code — especially when the user's request seems to violate one of these fundamentals.
|
||||
---
|
||||
|
||||
# Engineering Principles
|
||||
|
||||
These are the non-negotiable foundations. Every other skill in this plugin is an elaboration of one or more of these principles. They apply to every line of code, every language, every framework, every project.
|
||||
|
||||
---
|
||||
|
||||
## 1. Correctness > Speed > Cleverness
|
||||
|
||||
Correct code is the only code that matters. Fast code that's wrong is worse than slow code that's right. Clever code that's hard to understand is technical debt, even if it's correct and fast.
|
||||
|
||||
- A correct, readable solution is better than a clever, unreadable one.
|
||||
- Optimize only after profiling proves a bottleneck (see Principle 20).
|
||||
- "It works but it's ugly" is a reason to refactor, not to ship.
|
||||
|
||||
## 2. YAGNI — You Ain't Gonna Need It
|
||||
|
||||
Build for proven needs, not speculation. Every feature you add but never use is debt, not an asset.
|
||||
|
||||
- Don't add abstractions, parameters, or extension points until you have a concrete second consumer.
|
||||
- "We might need this later" is not a justification. Delete it. Later can scaffold for itself.
|
||||
- If you haven't needed it yet, you don't know what shape "later" actually needs.
|
||||
|
||||
## 3. Boy Scout Rule
|
||||
|
||||
Leave every module cleaner than you found it — even if just by renaming one variable or extracting one tiny function.
|
||||
|
||||
- Cumulative small improvements prevent code rot.
|
||||
- "I was just in there to fix a bug" is exactly when to leave it cleaner.
|
||||
- No, you don't need a ticket. Just fix it.
|
||||
|
||||
## 4. Root Cause, Not Symptom
|
||||
|
||||
Fix bugs at the root cause, not at every call site. One guard in the shared function is a smaller diff than a guard in every caller.
|
||||
|
||||
- Before patching a caller, grep all callers of the function you're about to touch.
|
||||
- Patching only the path the ticket names leaves every sibling caller still broken.
|
||||
- A symptom fix is not a fix — it's a second bug waiting for its sibling to trigger.
|
||||
|
||||
## 5. KISS — Keep It Simple, Stupid
|
||||
|
||||
The simplest solution that works is the correct solution. Not the smartest, not the most extensible, not the most abstract.
|
||||
|
||||
- Simple code is easy to change. Complex code is impossible to change.
|
||||
- If a solution is hard to explain, it's probably wrong for this problem.
|
||||
- Complexity is never justified by "we'll need it later" (see Principle 2).
|
||||
|
||||
## 6. DRY — Don't Repeat Yourself
|
||||
|
||||
Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.
|
||||
|
||||
- Duplication is the #1 code smell. Hunt it everywhere.
|
||||
- Not just code: duplicate configuration, duplicate documentation, duplicate logic.
|
||||
- Three strikes and you extract. Once is coincidence. Twice is suspicious. Three times is a pattern.
|
||||
|
||||
## 7. Tell, Don't Ask
|
||||
|
||||
Tell objects what to do instead of asking for their data and deciding yourself.
|
||||
|
||||
- `if (user.isActive()) user.sendNotification(msg)` — tell the user to notify.
|
||||
- `if (order.getStatus() === 'paid') order.ship()` — ask the order if it can ship.
|
||||
- The point is to keep logic with the data it operates on, not scattered across callers.
|
||||
|
||||
## 8. Command-Query Separation
|
||||
|
||||
A function either *does* something (command) or *answers* something (query), never both.
|
||||
|
||||
- `saveAndReturn()` violates CQS. Split into `save()` and `get()`.
|
||||
- This is not pedantry — combined CQ functions cause hidden side effects that make code unpredictable.
|
||||
|
||||
## 9. Meaningful Names
|
||||
|
||||
If you cannot name it, you don't understand it. A long descriptive name is better than a long descriptive comment.
|
||||
|
||||
- Names should answer: what is this, why does it exist, how is it used?
|
||||
- `int d` is never acceptable. `int elapsedTimeInDays` is.
|
||||
- Names are the single highest-leverage readability tool. Spend time on them.
|
||||
|
||||
## 10. One Function, One Responsibility
|
||||
|
||||
A function does one thing if, and only if, you cannot extract another function from it. Target ~20 lines. Smaller is better.
|
||||
|
||||
- If you can't see the whole function on one screen, it's too long.
|
||||
- One level of abstraction per function. The Step-Down Rule: callers above, callees below.
|
||||
- No flag parameters (`render(true)`). Split into `renderForSuite()` and `renderForSingleTest()`.
|
||||
|
||||
## 11. Tests Come First
|
||||
|
||||
Code without tests is legacy code. Not "needs tests" — *legacy*.
|
||||
|
||||
- **Three Laws of TDD:** ① Write no production code without a failing test. ② Write no more test than enough to fail. ③ Write no more production code than enough to pass.
|
||||
- If you can't write a test for a piece of code, the code is coupled to things it shouldn't be.
|
||||
- Test code is first-class code. Same quality, same review standards.
|
||||
|
||||
## 12. Comments = Failure of Expression
|
||||
|
||||
Before writing a comment, ask: can I rename or extract to make this unnecessary? Good comments explain *why*, not *what*.
|
||||
|
||||
- **Good comments (rare):** intent, warnings of consequences, legal headers, regex explanations, TODOs (pruned regularly).
|
||||
- **Bad comments (delete on sight):** redundant, journaling, closing-brace, commented-out code, mandated noise.
|
||||
- "Don't comment bad code — rewrite it." — Brian Kernighan
|
||||
|
||||
## 13. Less Is More
|
||||
|
||||
Deletion is better than addition. No unrequested abstractions. The best code is the code never written.
|
||||
|
||||
- An interface with one implementation is speculative. A factory for one product is speculative. A config value that never changes is speculative.
|
||||
- Before writing anything, ask: does this need to exist at all? Is it already in the codebase? Does the standard library do it?
|
||||
- Rung-by-rung: stdlib → installed deps → one line → minimum code.
|
||||
|
||||
## 14. Respect Boundaries
|
||||
|
||||
Business code must never depend on frameworks, databases, or UI. Architecture is about use cases.
|
||||
|
||||
- Domain layer: zero framework imports. Pure types, pure business rules.
|
||||
- The database is a detail. The web framework is a detail. The UI is a detail.
|
||||
- Swap the database without touching business logic. If you cannot, your boundary is violated.
|
||||
|
||||
## 15. Work Professionally
|
||||
|
||||
Never ship code you know is wrong. The only way to go fast is to go well.
|
||||
|
||||
- "We'll clean it up later" never happens. Dirty code slows the whole team down.
|
||||
- A professional says no to unreasonable deadlines rather than shipping garbage.
|
||||
- Every time you compromise on quality knowingly, you accumulate compound-interest debt.
|
||||
|
||||
## 16. Tech Debt — Don't Abandon Code
|
||||
|
||||
Finding an existing error or bad code is not a reason to skip it and say "that was already there." **Rot does not become correct because it's old.** Fix it or file a task.
|
||||
|
||||
- "It was like that when I got here" is not an acceptable engineering justification.
|
||||
- Code abandoned today is a production incident waiting for a trigger.
|
||||
- If you can't fix it right now, make sure there's a tracker entry and move on — but don't pretend it doesn't exist.
|
||||
|
||||
## 17. Principle of Least Astonishment
|
||||
|
||||
Code should be consistent and predictable. Don't make the reader think "wait, what?" Same formatting, same idioms, same patterns across the codebase.
|
||||
|
||||
- Surprise in birthday parties is fun. Surprise in production code is not.
|
||||
- Follow the existing patterns in the codebase, even if you'd write it differently.
|
||||
- Consistency within a project beats any individual preference.
|
||||
|
||||
## 18. Fail Fast — Fail Quickly, Fail Clearly
|
||||
|
||||
Validate at the boundary. Crash early with a clear message rather than silently proceeding with corrupted data and crashing 10 steps later with a cryptic error.
|
||||
|
||||
- `if (x == null) return x` is not fail-fast — it's hiding a bug.
|
||||
- Validate and reject at the API boundary, service boundary, function boundary.
|
||||
- A clear error message at the point of failure is cheaper than a stack trace from production that requires bisecting.
|
||||
|
||||
## 19. Explicit > Implicit
|
||||
|
||||
Don't hide behind magic, side effects, or hidden state. Code should say what it does. Implicit is an invitation for bugs that are invisible until production.
|
||||
|
||||
- No hidden side effects in functions named as queries.
|
||||
- No global state mutations. No surprise mutations. No "it just works" magic.
|
||||
- If someone reading the code can't see the effect, it's implicit — and that's a bug waiting to happen.
|
||||
|
||||
## 20. Premature Optimization is the Root of All Evil
|
||||
|
||||
"Make it work, make it right, make it fast" — in that order. Optimization before measurement and proven bottleneck is just complexity with no return.
|
||||
|
||||
- This is YAGNI for performance. You don't know what to optimize until you measure.
|
||||
- A cache that's never hit is code you have to maintain for zero benefit.
|
||||
- First make it correct. Then make it clean. Only then, if it's slow, make it fast.
|
||||
|
||||
## 21. Single Responsibility for Modules
|
||||
|
||||
Not just functions — classes, services, and modules must have one reason to change.
|
||||
|
||||
- A module that does everything is a god object/god service, and god objects are disasters waiting to happen.
|
||||
- If you can't describe what a module does in one sentence without "and," it has too many responsibilities.
|
||||
- SRP's evolved formulation: "responsible to one, and only one, actor."
|
||||
|
||||
## 22. Open/Closed Principle
|
||||
|
||||
Open for extension, closed for modification. Add features by writing new code, not by editing working, tested code.
|
||||
|
||||
- Strategy pattern over if-else chains for varying behavior.
|
||||
- Plugin architecture over switch statements for feature toggles.
|
||||
- Polymorphism over type checks.
|
||||
|
||||
## 23. Prefer Composition Over Inheritance
|
||||
|
||||
Inheritance makes code rigid and fragile — a change in the parent can break every child. Composition is more flexible, easier to test, and doesn't create class hierarchies that collapse at generation 3.
|
||||
|
||||
- "Is-a" relationships are rare. "Has-a" relationships are common.
|
||||
- Favor small interfaces composed together over deep class hierarchies.
|
||||
- If you're reaching for inheritance, ask: could I pass this as a dependency instead?
|
||||
|
||||
## 24. Atomic Commits — Small and Focused
|
||||
|
||||
One commit = one logical change. Not a mix of refactor + feature + bug fix in one commit.
|
||||
|
||||
- A clean diff means a fast review and a readable history.
|
||||
- If your commit message needs "and," your commit is too large.
|
||||
- `git add -p` is your friend. Stage related changes together, unrelated changes separately.
|
||||
@@ -0,0 +1,149 @@
|
||||
---
|
||||
name: error-handling
|
||||
description: Best practices for error handling across languages — exceptions, Result types, input validation, error boundaries, null safety, and observability. Use when designing error strategies, writing validation logic, handling API errors, or whenever the user mentions "error handling," "exception," "try-catch," "Result," "Option," "Either," "null check," "validation," "panic," or "error boundary."
|
||||
---
|
||||
|
||||
# Error Handling
|
||||
|
||||
Good error handling makes failures predictable, debuggable, and safe.
|
||||
|
||||
## Core Principles
|
||||
|
||||
1. **Fail fast** — detect and report errors at the nearest boundary.
|
||||
2. **Never swallow errors** — empty catches, ignored error returns, and silent fallbacks hide bugs.
|
||||
3. **Errors are values** — propagate them explicitly (Result types, error returns) over exceptions for normal code paths.
|
||||
4. **Recoverable vs unrecoverable** — use Result/Option/recovery for expected failures; crash for truly unrecoverable states.
|
||||
|
||||
## Pattern by Context
|
||||
|
||||
```
|
||||
Context Layer │ Pattern
|
||||
──────────────────┼──────────────────────
|
||||
Domain / Business │ Result types — expected business logic failures
|
||||
Application │ Exceptions (wrapped) — infrastructure failures
|
||||
API Boundary │ Caught + mapped to error responses
|
||||
UI Layer │ Error boundaries / graceful degradation
|
||||
```
|
||||
|
||||
### Use Result Types (preferred) for Business Logic
|
||||
```typescript
|
||||
// TypeScript — discriminated union
|
||||
type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };
|
||||
|
||||
function createOrder(input: unknown): Result<Order, ValidationError> {
|
||||
if (!input || typeof input !== 'object') return { ok: false, error: { field: 'input', message: 'Invalid' } };
|
||||
return { ok: true, value: new Order(input) };
|
||||
}
|
||||
```
|
||||
|
||||
```rust
|
||||
// Rust — native Result
|
||||
fn create_order(input: CreateOrderInput) -> Result<Order, ValidationError> {
|
||||
validate(input)?;
|
||||
Ok(Order::new(input))
|
||||
}
|
||||
```
|
||||
|
||||
```python
|
||||
# Python — custom exceptions for recoverable failures
|
||||
class ValidationError(Exception): ...
|
||||
class NotFoundError(Exception): ...
|
||||
|
||||
def create_order(input: dict) -> Order:
|
||||
if not input.get("items"):
|
||||
raise ValidationError("items required")
|
||||
return Order(items=input["items"])
|
||||
```
|
||||
|
||||
### Use Exceptions for Infrastructure Failures
|
||||
```typescript
|
||||
class DatabaseConnectionError extends Error {
|
||||
constructor(public readonly cause: unknown) {
|
||||
super('Database connection failed');
|
||||
}
|
||||
}
|
||||
|
||||
class CreateOrderUseCase {
|
||||
async execute(input: CreateOrderInput): Promise<Result<Order, AppError>> {
|
||||
try {
|
||||
const user = await this.userRepo.findById(input.userId);
|
||||
if (!user) return { ok: false, error: new NotFoundError('User') };
|
||||
return { ok: true, value: Order.create(user, input.items) };
|
||||
} catch (e) {
|
||||
throw new DatabaseConnectionError(e); // wrap tech errors
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Input Validation
|
||||
|
||||
- **Validate at system boundaries** — API entry points, CLI args, file reads, form submissions.
|
||||
- **Use validation libraries** — Zod (TS), Pydantic (Python), serde (Rust), go-playground/validator.
|
||||
- **Don't validate in domain entities** — validate at boundary, pass typed objects inward.
|
||||
- **Fail early** — validate all fields, return all errors, not just the first.
|
||||
|
||||
```typescript
|
||||
// TS with Zod
|
||||
const CreateUserSchema = z.object({
|
||||
email: z.string().email(),
|
||||
age: z.number().int().positive().max(150),
|
||||
});
|
||||
type CreateUserInput = z.infer<typeof CreateUserSchema>;
|
||||
|
||||
app.post('/users', (c) => {
|
||||
const parsed = CreateUserSchema.safeParse(await c.req.json());
|
||||
if (!parsed.success) return c.json({ errors: parsed.error.flatten() }, 400);
|
||||
const result = await createUserUseCase.execute(parsed.data);
|
||||
if (!result.ok) return c.json({ error: result.error.message }, 422);
|
||||
return c.json(result.value, 201);
|
||||
});
|
||||
```
|
||||
|
||||
## Null Safety
|
||||
|
||||
- **Don't return null** — return `Option<T>`, `undefined`, empty collection, or throw.
|
||||
- **Don't accept null** — fail fast at the boundary if a parameter is required.
|
||||
- **Languages with null safety**: enable strict mode (TypeScript `strict`, Kotlin, Swift, Rust).
|
||||
- **Languages without**: use `Optional` wrappers.
|
||||
|
||||
## Error Boundaries (UI)
|
||||
|
||||
```typescript
|
||||
// React — catch rendering errors
|
||||
class ErrorBoundary extends React.Component {
|
||||
state = { error: null };
|
||||
static getDerivedStateFromError(error: Error) {
|
||||
return { error };
|
||||
}
|
||||
render() {
|
||||
if (this.state.error) return <ErrorFallback error={this.state.error} />;
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Observability in Errors
|
||||
|
||||
- **Every error should be logged** with context: operation, input (sanitized), stack trace.
|
||||
- **Structured logging** — machine-readable error fields, not just strings.
|
||||
- **Correlation IDs** — trace errors across services (Dapr/Jaeger trace ID).
|
||||
- **Never log secrets** — sanitize errors before logging.
|
||||
|
||||
## Language Quick Reference
|
||||
|
||||
| Language | Pattern | Null Safety |
|
||||
|----------|---------|-------------|
|
||||
| TypeScript | Result unions or exceptions | `strict: true`, optional chaining |
|
||||
| Python | Exceptions | Optional[x] (type hint only) |
|
||||
| Rust | `Result<T, E>`, `Option<T>` | Ownership system |
|
||||
| Go | `value, err := f()` | No — check `err != nil` |
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- ❌ **Empty catch** (`catch(e) {}`) — hides errors.
|
||||
- ❌ **Swallowing errors** — returning default values silently.
|
||||
- ❌ **Using exceptions for control flow** — exceptions should never be expected.
|
||||
- ❌ **Generic error messages** — `"Something went wrong"` with no context.
|
||||
- ❌ **Mixing error strategies** — some functions return null, some throw, some return Result.
|
||||
- ❌ **Too broad catches** — `catch (Exception e)` catches absolutely everything.
|
||||
@@ -0,0 +1,132 @@
|
||||
---
|
||||
name: git-workflow
|
||||
description: Git workflow best practices — commit conventions, branching strategies, PR conventions, rebase vs merge, and code review. Use when writing commit messages, reviewing PRs, planning branching strategy, or whenever the user mentions "commit," "branch," "pull request," "PR," "merge," "rebase," "squash," "git flow," "trunk-based," "conventional commit," or "code review."
|
||||
---
|
||||
|
||||
# Git Workflow
|
||||
|
||||
## Commit Message Convention
|
||||
|
||||
Format: `<type>(<scope>): <description>`
|
||||
|
||||
```
|
||||
feat(auth): add google oauth sign-in
|
||||
fix(billing): handle null currency in invoice
|
||||
chore(deps): update typescript to 5.5
|
||||
docs(api): document rate-limit headers
|
||||
refactor(orders): extract payment validation
|
||||
test(users): add unit tests for CreateUser
|
||||
ci(deploy): split build and push steps
|
||||
perf(db): add index on orders.created_at
|
||||
style(ui): fix button alignment
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
- Type is lowercase. No period at the end.
|
||||
- Scope is required — the affected module/context.
|
||||
- Imperative mood ("add" not "added" or "adds").
|
||||
- Subject under 72 chars.
|
||||
- Body wraps at 72 chars. Explains *why* (not *what*).
|
||||
- Footer for breaking changes: `BREAKING CHANGE: ...`
|
||||
- Footer for co-authors: `Co-Authored-By: Name <email>`
|
||||
|
||||
## Branching Strategy
|
||||
|
||||
### Trunk-Based (preferred for CI/CD)
|
||||
```
|
||||
main ← feature branches
|
||||
```
|
||||
- Short-lived feature branches (1-2 days max).
|
||||
- PR → auto-merge to main after CI passes.
|
||||
- Deploy from main. Hotfix = branch from main → merge back.
|
||||
|
||||
### GitHub Flow
|
||||
```
|
||||
main → feature/xyz → PR → main
|
||||
main → fix/xyz → PR → main
|
||||
main → chore/xyz → PR → main
|
||||
```
|
||||
|
||||
### Git Flow (for release-based projects)
|
||||
```
|
||||
main → develop → feature/xyz → PR → develop
|
||||
→ release/v1.2 → main + develop
|
||||
→ hotfix/v1.2.1 → main + develop
|
||||
```
|
||||
|
||||
**Choose:**
|
||||
- **Trunk-based** — if you deploy multiple times a day (SaaS, web apps).
|
||||
- **Git Flow** — if you version releases (libraries, mobile apps, on-prem).
|
||||
|
||||
## PR Conventions
|
||||
|
||||
### Title
|
||||
Same as commit convention: `feat(scope): description`
|
||||
|
||||
### Description Template
|
||||
```
|
||||
## What
|
||||
Brief description of the change.
|
||||
|
||||
## Why
|
||||
Problem being solved. Link to issue/ticket.
|
||||
|
||||
## How
|
||||
High-level approach — architecture decisions, trade-offs.
|
||||
|
||||
## Testing
|
||||
- [ ] Unit tests added/passed
|
||||
- [ ] Integration tests added/passed
|
||||
- [ ] Manual test steps
|
||||
|
||||
## Screenshots (if UI change)
|
||||
...
|
||||
|
||||
## Checklist
|
||||
- [ ] Lint passes
|
||||
- [ ] Tests pass
|
||||
- [ ] Docs updated
|
||||
- [ ] Breaking changes documented
|
||||
```
|
||||
|
||||
### PR Size
|
||||
- **Target: <200 lines changed.** Large PRs get less thorough reviews.
|
||||
- If >500 lines, split into logical chunks or mark as "stacked PR."
|
||||
- One logical change per PR. Don't mix refactors with features.
|
||||
|
||||
## Code Review
|
||||
|
||||
### Reviewer Checklist
|
||||
1. [ ] Does the solution match the PR description?
|
||||
2. [ ] Any edge cases unhandled? (empty state, errors, concurrency)
|
||||
3. [ ] Tests cover happy path + error paths + edge cases?
|
||||
4. [ ] No dead code, commented-out code, magic numbers?
|
||||
5. [ ] Dependencies are necessary (no scope creep)?
|
||||
6. [ ] Error handling appropriate (not silent, not leaky)?
|
||||
7. [ ] Security: input validated? Auth checked? No secrets?
|
||||
|
||||
### Review Etiquette
|
||||
- **Be specific** — "Line 42: this query is N+1, use `JOIN`" not "this is slow"
|
||||
- **Ask, don't demand** — "Should this be a named constant?" not "Make this a constant"
|
||||
- **Approve quickly for trivial changes** — don't block for style nits
|
||||
- **Distinguish blocking vs nit** — explicitly label `nit:` or `blocking:`
|
||||
- **Respond to reviews** — every comment gets a reply or action
|
||||
|
||||
## Merging Strategy
|
||||
|
||||
| Strategy | When |
|
||||
|----------|------|
|
||||
| **Squash merge** | One feature = one commit on main. Clean history. |
|
||||
| **Rebase merge** | Preserve individual commits. For stacked PRs. |
|
||||
| **Merge commit** | Preserves full history of feature branch. Noisy. |
|
||||
|
||||
**Default: squash merge.** Keeps main clean and bisectable.
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- ❌ **Committing to main directly** — always use PRs (except hotfix emergencies)
|
||||
- ❌ **Large, unfocused commits** — `"WIP"`, `"fixes"`, `"misc changes"`
|
||||
- ❌ **Rebasing shared branches** — never rebase a branch others have pulled
|
||||
- ❌ **Merge commits in main** — unless you use merge-commit strategy deliberately
|
||||
- ❌ **Stale branches** — clean up after merge. Name indicates age/staleness
|
||||
- ❌ **No issue/PR reference** — every commit should answer "why?"
|
||||
@@ -0,0 +1,213 @@
|
||||
---
|
||||
name: go
|
||||
description: Go best practices — idiomatic Go, project layout, error handling, interfaces, concurrency, testing, and dependency management. Use when writing Go code, reviewing Go projects, or whenever the user mentions "Go," "Golang," "goroutine," "channel," "interface," "defer," "go mod," "gRPC," "net/http," "chi," "echo," "fiber," or "go test."
|
||||
---
|
||||
|
||||
# Go Best Practices
|
||||
|
||||
## Project Layout
|
||||
|
||||
```
|
||||
project/
|
||||
├── cmd/
|
||||
│ └── server/
|
||||
│ └── main.go # Entry point
|
||||
├── internal/
|
||||
│ ├── domain/ # Business entities, value objects
|
||||
│ ├── application/ # Use cases, ports
|
||||
│ ├── infrastructure/ # DB, HTTP, cache adapters
|
||||
│ └── api/ # HTTP handlers, middleware
|
||||
├── pkg/ # Public library code (for external consumption)
|
||||
├── tests/ # Integration/E2E tests
|
||||
├── go.mod
|
||||
├── go.sum
|
||||
└── Makefile
|
||||
```
|
||||
|
||||
- `cmd/` — one directory per binary. Small main function, defer to package.
|
||||
- `internal/` — not importable outside this module. Clean architecture layering.
|
||||
- `pkg/` — safe for external packages to import.
|
||||
|
||||
## Idiomatic Go
|
||||
|
||||
```go
|
||||
// Named returns are OK but prefer bare in simple cases
|
||||
func Parse(input string) (result Result, err error) {
|
||||
if input == "" {
|
||||
return result, errors.New("empty input")
|
||||
}
|
||||
result.Value = input
|
||||
return
|
||||
}
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
- **Favor files over functions** — one logical unit per file, not one function per file.
|
||||
- **Short variable names** — `ctx`, `r` (request), `w` (response writer), `cfg` (config).
|
||||
- **Getters don't have `Get` prefix** — `user.Name()` not `user.GetName()`.
|
||||
- **Zero-value is useful** — initialize your structs so zero value is usable.
|
||||
- **`gofmt` is the law** — no style debates. Run `gofmt` (or `gofumpt`).
|
||||
- **`go vet` before commit** — catches subtle bugs.
|
||||
|
||||
## Error Handling
|
||||
|
||||
```go
|
||||
// Always handle errors. No discard.
|
||||
result, err := doSomething()
|
||||
if err != nil {
|
||||
return fmt.Errorf("doing something: %w", err) // wrap with context
|
||||
}
|
||||
|
||||
// Sentinel errors for specific cases
|
||||
var ErrNotFound = errors.New("not found")
|
||||
|
||||
// Custom error types for additional context
|
||||
type ValidationError struct {
|
||||
Field string
|
||||
Err error
|
||||
}
|
||||
func (e *ValidationError) Error() string {
|
||||
return fmt.Sprintf("validation failed on %s: %v", e.Field, e.Err)
|
||||
}
|
||||
func (e *ValidationError) Unwrap() error { return e.Err }
|
||||
```
|
||||
|
||||
- **Error wrapping** — `fmt.Errorf("context: %w", err)` for the call chain.
|
||||
- **`errors.Is()`** for sentinel errors. **`errors.As()`** for custom types.
|
||||
- **Don't use `_` to discard errors** — unless truly intentional (e.g. `fmt.Fprint`).
|
||||
- **Defer for cleanup** — `defer file.Close()`, `defer mu.Unlock()`.
|
||||
|
||||
## Interfaces
|
||||
|
||||
```go
|
||||
// Define interfaces in the consumer package (application/domain), not producer
|
||||
type UserRepository interface {
|
||||
FindByID(ctx context.Context, id string) (*User, error)
|
||||
Save(ctx context.Context, user *User) error
|
||||
}
|
||||
|
||||
// Small interfaces are Go's superpower
|
||||
type Reader interface { Read(p []byte) (n int, err error) }
|
||||
type Writer interface { Write(p []byte) (n int, err error) }
|
||||
type Stringer interface { String() string }
|
||||
```
|
||||
|
||||
- **Accept interfaces, return structs** — consumer declares what it needs.
|
||||
- **Interface satisfaction is implicit** — no `implements` keyword.
|
||||
- **Prefer single-method interfaces (IO pattern).**
|
||||
- **Don't export interfaces:**
|
||||
|
||||
## Concurrency
|
||||
|
||||
```go
|
||||
// Goroutines + channels for communication
|
||||
func processBatch(ctx context.Context, items []Item) error {
|
||||
results := make(chan Result, len(items))
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
// Fan-out
|
||||
for _, item := range items {
|
||||
item := item // copy for closure
|
||||
go func() {
|
||||
select {
|
||||
case results <- process(item):
|
||||
case <-ctx.Done():
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Fan-in
|
||||
for range items {
|
||||
select {
|
||||
case r := <-results:
|
||||
if r.err != nil { return r.err }
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// sync.WaitGroup for waiting
|
||||
var wg sync.WaitGroup
|
||||
for _, v := range items {
|
||||
wg.Add(1)
|
||||
go func(v Item) {
|
||||
defer wg.Done()
|
||||
process(v)
|
||||
}(v)
|
||||
}
|
||||
wg.Wait()
|
||||
```
|
||||
|
||||
- **Don't communicate by sharing memory; share memory by communicating.**
|
||||
- **Channel or mutex?** — channel when passing ownership or signaling; mutex for shared state.
|
||||
- **Context first param** — `ctx context.Context` is always the first function parameter.
|
||||
- **Never start goroutines without knowing when they stop.**
|
||||
- **Use `errgroup`** for concurrent operations that share context.
|
||||
|
||||
## Testing
|
||||
|
||||
```go
|
||||
func TestCreateUser(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Table-driven tests
|
||||
tests := []struct {
|
||||
name string
|
||||
input CreateUserInput
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "valid user", input: CreateUserInput{Email: "a@b.com"}, wantErr: false},
|
||||
{name: "empty email", input: CreateUserInput{}, wantErr: true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt // capture for t.Parallel
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
repo := NewInMemoryUserRepo() // fake
|
||||
svc := NewUserService(repo)
|
||||
err := svc.CreateUser(context.Background(), tt.input)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("got err = %v, wantErr = %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkCreateUser(b *testing.B) {
|
||||
for b.Loop() { // go 1.24+
|
||||
repo := NewInMemoryUserRepo()
|
||||
svc := NewUserService(repo)
|
||||
if err := svc.CreateUser(context.Background(), validInput); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- **`testing/slog`** for test logging. **`testing/fstest`** for filesystem fakes.
|
||||
- **Fakes over mocks** — in-memory implementations are simpler and more maintainable.
|
||||
- **`testify`** is common but stdlib `testing` has improved — prefer `got/want` comparison.
|
||||
|
||||
## Standard Library First
|
||||
|
||||
Go's stdlib is extensive. Reach for it before frameworks:
|
||||
- `net/http` — for basic HTTP servers (chi/echo for routing, but stdlib for the basics).
|
||||
- `encoding/json` — JSON serialization (better than most third-party libs).
|
||||
- `database/sql` — database access (with `pgx` for Postgres).
|
||||
- `testing` — built-in test runner, fuzzing, benchmarks.
|
||||
- `context` — cancellation, deadlines, request-scoped values.
|
||||
- `slog` — structured logging (go 1.21+).
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- ❌ `context.Background()` in handlers — use `req.Context()`
|
||||
- ❌ Global variables — pass dependencies explicitly
|
||||
- ❌ `init()` functions — they break control flow and test isolation
|
||||
- ❌ `_ = foo()` — silent error discard; handle or at least log
|
||||
- ❌ Embedding HTTP server logic — `ListenAndServe` in `main.go`, not hidden in a library
|
||||
- ❌ Deeply nested if-err chains — flatten with early returns
|
||||
- ❌ `interface{}` / `any` where concrete type works
|
||||
@@ -0,0 +1,201 @@
|
||||
---
|
||||
name: hono-backend
|
||||
description: Hono best practices — middleware, RPC, validation (Zod), Drizzle integration, and project organization. Use when building Hono backend APIs, or whenever the user mentions "Hono," "Hono.js," "hono RPC," "hono middleware," "hono Zod OpenAPI," "hono validation," or "hono Drizzle."
|
||||
---
|
||||
|
||||
# Hono Backend Best Practices
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── modules/ # Feature modules
|
||||
│ ├── users/
|
||||
│ │ ├── routes.ts # Hono routes (thin — validation + delegation)
|
||||
│ │ ├── service.ts # Business logic / use cases
|
||||
│ │ └── repository.ts # Data access (Drizzle queries)
|
||||
│ └── orders/
|
||||
├── middleware/ # Custom middleware
|
||||
├── lib/ # Shared utilities (env, JWT, hashing)
|
||||
├── db/ # Drizzle schema, migrations
|
||||
└── index.ts # App entry + composition root
|
||||
```
|
||||
|
||||
## Route Definition
|
||||
|
||||
```typescript
|
||||
import { Hono } from 'hono';
|
||||
import { z } from 'zod';
|
||||
import { zValidator } from '@hono/zod-validator';
|
||||
import { userService } from './service';
|
||||
import type { Env } from '../lib/env';
|
||||
|
||||
const users = new Hono<Env>()
|
||||
.get('/', async (c) => {
|
||||
const page = Number(c.req.query('page') || '1');
|
||||
const limit = Number(c.req.query('limit') || '20');
|
||||
const result = await userService.list({ page, limit });
|
||||
return c.json(result);
|
||||
})
|
||||
.post('/', zValidator('json', CreateUserSchema), async (c) => {
|
||||
const body = c.req.valid('json');
|
||||
const user = await userService.create(body);
|
||||
return c.json(user, 201);
|
||||
})
|
||||
.get('/:id', async (c) => {
|
||||
const id = c.req.param('id');
|
||||
const user = await userService.findById(id);
|
||||
if (!user) return c.json({ error: 'Not found' }, 404);
|
||||
return c.json(user);
|
||||
});
|
||||
|
||||
export { users };
|
||||
```
|
||||
|
||||
## Zod Validation
|
||||
|
||||
```typescript
|
||||
import { z } from 'zod';
|
||||
|
||||
// Define at the boundary
|
||||
export const CreateUserSchema = z.object({
|
||||
email: z.string().email(),
|
||||
name: z.string().min(1).max(100).optional(),
|
||||
role: z.enum(['admin', 'user']).default('user'),
|
||||
});
|
||||
|
||||
export const PaginationSchema = z.object({
|
||||
page: z.coerce.number().int().positive().default(1),
|
||||
limit: z.coerce.number().int().min(1).max(100).default(20),
|
||||
});
|
||||
|
||||
// Infer types
|
||||
export type CreateUserInput = z.infer<typeof CreateUserSchema>;
|
||||
```
|
||||
|
||||
## Hono RPC (Type-Safe Client)
|
||||
|
||||
```typescript
|
||||
// Server
|
||||
import { hono } from 'hono';
|
||||
import { routes } from './routes';
|
||||
|
||||
const app = new Hono().route('/api', routes);
|
||||
export type App = typeof app;
|
||||
|
||||
// Client (no treaty needed — direct fetch wrapper)
|
||||
import { hc } from 'hono/client';
|
||||
import type { App } from '../server';
|
||||
|
||||
const client = hc<App>('http://localhost:3000');
|
||||
const res = await client.api.users.$post({
|
||||
json: { email: 'test@example.com', name: 'Alice' },
|
||||
});
|
||||
// res is fully typed — status codes, response body
|
||||
```
|
||||
|
||||
## Middleware
|
||||
|
||||
```typescript
|
||||
import { Hono } from 'hono';
|
||||
|
||||
const app = new Hono()
|
||||
// Built-in
|
||||
.use('*', cors())
|
||||
.use('*', logger())
|
||||
|
||||
// Custom
|
||||
.use('*', async (c, next) => {
|
||||
const start = Date.now();
|
||||
await next();
|
||||
const ms = Date.now() - start;
|
||||
c.header('X-Response-Time', `${ms}ms`);
|
||||
})
|
||||
|
||||
// Auth middleware
|
||||
.use('/api/*', async (c, next) => {
|
||||
const auth = c.req.header('Authorization');
|
||||
if (!auth?.startsWith('Bearer ')) return c.json({ error: 'Unauthorized' }, 401);
|
||||
const user = await verifyToken(auth.slice(7));
|
||||
if (!user) return c.json({ error: 'Invalid token' }, 401);
|
||||
c.set('user', user);
|
||||
await next();
|
||||
});
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
```typescript
|
||||
import { HTTPException } from 'hono/http-exception';
|
||||
|
||||
app.onError((err, c) => {
|
||||
if (err instanceof HTTPException) {
|
||||
return c.json({ error: err.message }, err.status);
|
||||
}
|
||||
console.error(err); // log unexpected errors
|
||||
return c.json({ error: 'Internal server error' }, 500);
|
||||
});
|
||||
|
||||
// In routes
|
||||
app.post('/orders', async (c) => {
|
||||
const result = await orderService.create(body);
|
||||
if (!result.ok) {
|
||||
throw new HTTPException(422, { message: result.error.message });
|
||||
}
|
||||
return c.json(result.value, 201);
|
||||
});
|
||||
```
|
||||
|
||||
## OpenAI / Swagger Integration
|
||||
|
||||
```typescript
|
||||
import { OpenAPIHono } from '@hono/zod-openapi';
|
||||
|
||||
const app = new OpenAPIHono();
|
||||
|
||||
app.openapi(
|
||||
createRoute({
|
||||
method: 'post',
|
||||
path: '/users',
|
||||
request: { body: { content: { 'application/json': { schema: CreateUserSchema } } } },
|
||||
responses: {
|
||||
201: { description: 'User created', content: { 'application/json': { schema: UserSchema } } },
|
||||
422: { description: 'Validation error' },
|
||||
},
|
||||
}),
|
||||
async (c) => {
|
||||
const body = c.req.valid('json');
|
||||
const user = await userService.create(body);
|
||||
return c.json(user, 201);
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
```typescript
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import { app } from '../src/index';
|
||||
|
||||
describe('users', () => {
|
||||
it('creates a user', async () => {
|
||||
const res = await app.request('/users', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email: 'test@example.com' }),
|
||||
});
|
||||
expect(res.status).toBe(201);
|
||||
const body = await res.json();
|
||||
expect(body.email).toBe('test@example.com');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- ❌ Business logic in route handlers — routes validate + delegate
|
||||
- ❌ No Zod validation on inputs — every route that accepts input must validate
|
||||
- ❌ `c.req.raw` instead of Hono's `c.req.json/query/valid`
|
||||
- ❌ Mixing Hono and Express middleware patterns
|
||||
- ❌ Global error handled only at route level — use app-level `onError`
|
||||
- ❌ Not using `c.set` for typed variables — declare `Env` type with bindings/variables
|
||||
@@ -0,0 +1,107 @@
|
||||
---
|
||||
name: hub-guide
|
||||
description: Guide for the Asepharyana Hub monorepo — submodule workflow, infrastructure stack (Traefik, Dapr, NATS, Redis), CI/CD pipelines, adding new services, and debugging tips. Use when working in the asepharyana-hub monorepo, managing submodules, dealing with Docker/infra setup, or whenever the user asks about "hub monorepo," "submodules," "Traefik," "Dapr," "NATS," "infrastructure," or "adding a new service."
|
||||
---
|
||||
|
||||
# Hub Guide — Asepharyana Hub Monorepo
|
||||
|
||||
## Submodule Workflow
|
||||
|
||||
- Code changes go in the submodule repo, not here. The hub monorepo only tracks submodule pointers.
|
||||
- After pushing changes to a submodule repo, update the pointer here:
|
||||
```bash
|
||||
cd apps/<name> && git checkout main && git pull
|
||||
cd ../.. && git add apps/<name> && git commit -m "chore(deps): update <name> submodule"
|
||||
```
|
||||
- CI/CD auto-updates submodule pointers via `repository_dispatch`. Manual updates are fine for dev.
|
||||
|
||||
### Typical Submodule State
|
||||
|
||||
| State | Meaning |
|
||||
|-------|---------|
|
||||
| `(HEAD)` | Detached HEAD — submodule is at the committed pointer |
|
||||
| `(main)` | On the default branch — you've done `cd apps/name && git checkout main` |
|
||||
| Dirty | Uncommitted changes inside submodule |
|
||||
|
||||
To reset a submodule to its committed pointer:
|
||||
```bash
|
||||
git submodule update --init --recursive apps/<name>
|
||||
```
|
||||
|
||||
## Development Quickstart
|
||||
|
||||
```bash
|
||||
make init-submodules # After fresh clone — fetches all submodules
|
||||
make dev # Start Redis for local dev
|
||||
docker compose -f infra/compose/shared.yml up -d # Full infra stack
|
||||
```
|
||||
|
||||
## Local vs Production
|
||||
|
||||
| Aspect | Local | Production (VPS) |
|
||||
|--------|-------|------------------|
|
||||
| DB | None (or local) | PostgreSQL on `imrnes` via Tailscale |
|
||||
| Redis | `make dev` | Container on `orangevps` |
|
||||
| Traefik | Not running | TLS-terminated on `orangevps` |
|
||||
| DNS | `localhost` | `*.asepharyana.my.id`, `*.asepharya.web.id` |
|
||||
|
||||
## Debugging Tips
|
||||
|
||||
### Docker compose validation
|
||||
```bash
|
||||
for f in infra/compose/*.yml; do docker compose -f "$f" config >/dev/null && echo "OK $f"; done
|
||||
```
|
||||
|
||||
### YAML syntax check
|
||||
```bash
|
||||
python -c "import pathlib, yaml; [yaml.safe_load(open(p)) for p in pathlib.Path('infra').rglob('*.yml')]"
|
||||
```
|
||||
|
||||
### Check submodule pointers
|
||||
```bash
|
||||
git submodule status
|
||||
# Leading `-` = not initialized, `+` = different from committed hash, ` ` = matches
|
||||
```
|
||||
|
||||
### Traefik route not working?
|
||||
1. Check `infra/traefik/dynamic/apps.yaml` — router rule + service definition present?
|
||||
2. Container labels in compose file include Traefik config?
|
||||
3. Container on `app-shared-net`?
|
||||
|
||||
## Adding a New Service — Checklist
|
||||
|
||||
1. [ ] Create separate repo for app code
|
||||
2. [ ] `git submodule add <url> apps/<name>`
|
||||
3. [ ] Create Dockerfile in `infra/docker/`
|
||||
4. [ ] Create compose file in `infra/compose/` (app + Dapr sidecar)
|
||||
5. [ ] Add Traefik router in `infra/traefik/dynamic/apps.yaml`
|
||||
6. [ ] Add build job in `.github/workflows/docker-build-push.yml`
|
||||
7. [ ] Verify: `docker compose -f infra/compose/<name>.yml config`
|
||||
|
||||
See `docs/add-new-app.md` for full guide.
|
||||
|
||||
## Monitoring
|
||||
|
||||
- **Dashboard**: `/dashboard` on the hub site (auto-refresh 15s)
|
||||
- **Dashboard API**: `/api/dashboard` — JSON with containers, traces, metrics
|
||||
- **Prometheus**: Auto-discovers containers with `prometheus.io/scrape=true` label via Docker SD
|
||||
- **Jaeger**: Traces via OTLP — check for cross-service latency
|
||||
|
||||
## Infrastructure Files Map
|
||||
|
||||
| Path | Purpose |
|
||||
|------|---------|
|
||||
| `infra/compose/*.yml` | One Docker Compose file per service |
|
||||
| `infra/dapr/components/` | Dapr pub/sub, state store component configs |
|
||||
| `infra/docker/*.Dockerfile` | Build files per service |
|
||||
| `infra/traefik/dynamic/apps.yaml` | Traefik route definitions |
|
||||
| `infra/traefik/traefik.yml` | Traefik static config (entrypoints, providers) |
|
||||
| `.github/workflows/` | CI/CD pipelines |
|
||||
| `docs/` | ADRs, deployment guide, new-app guide |
|
||||
|
||||
## Git Hook Scripts
|
||||
|
||||
Located in `scripts/`:
|
||||
- `scripts/cleanup.sh` — prune old Docker images, clean temp files
|
||||
- `scripts/update-deps.sh` — bump dependencies across submodules
|
||||
- `scripts/setup-hooks.sh` — install local git hooks
|
||||
@@ -0,0 +1,180 @@
|
||||
---
|
||||
name: logging-observability
|
||||
description: Best practices for logging, metrics, tracing, alerting, and observability — structured logging, correlation IDs, Prometheus metrics, distributed tracing, and dashboard design. Use when adding logging, designing monitoring, setting up dashboards, debugging production issues, or whenever the user mentions "logging," "observability," "metrics," "tracing," "Prometheus," "Grafana," "Jaeger," "OpenTelemetry," "structured logging," or "alerting."
|
||||
---
|
||||
|
||||
# Logging & Observability
|
||||
|
||||
## The Three Pillars
|
||||
|
||||
```
|
||||
Logs — discrete events: "user logged in at 10:32:14"
|
||||
Metrics — aggregatable numbers: 50 req/s, 200ms p99 latency
|
||||
Traces — request lifecycle across services: "order #123 took 400ms total"
|
||||
```
|
||||
|
||||
All three are needed. None replaces the others.
|
||||
|
||||
## Structured Logging
|
||||
|
||||
**Log in JSON format** — machine-readable, parsable, searchable.
|
||||
|
||||
```json
|
||||
// ❌ Unstructured (text search grey area)
|
||||
"User 123 created order 456 for $50.99"
|
||||
|
||||
// ✅ Structured (parseable, filterable)
|
||||
{
|
||||
"level": "info",
|
||||
"time": "2025-07-25T10:32:14Z",
|
||||
"message": "order created",
|
||||
"service": "order-service",
|
||||
"trace_id": "abc123def456",
|
||||
"user_id": "123",
|
||||
"order_id": "456",
|
||||
"amount": 50.99,
|
||||
"currency": "USD",
|
||||
"duration_ms": 45
|
||||
}
|
||||
```
|
||||
|
||||
### Log Levels — Use Consistently
|
||||
|
||||
| Level | When | Example |
|
||||
|-------|------|---------|
|
||||
| **ERROR** | Something is broken. Needs human attention. | DB connection failed, payment declined |
|
||||
| **WARN** | Something unexpected but recoverable. | Retry succeeded, rate limit approaching |
|
||||
| **INFO** | Notable lifecycle events. User-triggered actions. | Order created, user signed up, scheduled job ran |
|
||||
| **DEBUG** | Detailed context for debugging. Off in prod. | SQL query, request body, iteration details |
|
||||
| **TRACE** | Very fine-grained. For deep debugging only. | Function entry/exit, loop iterations |
|
||||
|
||||
### What to Log
|
||||
|
||||
- **Every error** with stack trace, context, and correlation ID.
|
||||
- **Every request** at INFO — method, path, status, duration.
|
||||
- **Business events** — state transitions (order created → paid → shipped).
|
||||
- **Auth events** — login success/failure, token refresh, privilege change.
|
||||
- **Third-party calls** — external API call duration, status.
|
||||
|
||||
### What NOT to Log (Security)
|
||||
|
||||
- ❌ Passwords, tokens, secrets, API keys
|
||||
- ❌ PII beyond necessity (email, phone, SSN, address)
|
||||
- ❌ Full request/response bodies (except DEBUG, and even then — sanitize)
|
||||
- ❌ Database connection strings
|
||||
- ❌ Internal IPs in public-facing systems
|
||||
|
||||
## Metrics (Prometheus)
|
||||
|
||||
### RED Method (for services)
|
||||
|
||||
| Metric | What | Good |
|
||||
|--------|------|------|
|
||||
| **Rate** | Requests per second | Flat line — load |
|
||||
| **Errors** | Failed requests / total | <1% (target), <0.1% (excellent) |
|
||||
| **Duration** | Latency distribution | p50 < 100ms, p99 < 500ms |
|
||||
|
||||
### USE Method (for infrastructure)
|
||||
|
||||
| Metric | What |
|
||||
|--------|------|
|
||||
| **Utilization** | CPU, RAM, disk, network bandwidth |
|
||||
| **Saturation** | Queue depth, swap usage, load average |
|
||||
| **Errors** | Disk IO errors, packet drops, OOM kills |
|
||||
|
||||
### Four Golden Signals (Google SRE)
|
||||
|
||||
1. **Latency** — time to serve a request
|
||||
2. **Traffic** — demand on the system (RPS, active users)
|
||||
3. **Errors** — explicit failures + implicit (200 with wrong data)
|
||||
4. **Saturation** — how "full" the system is
|
||||
|
||||
### Key Metrics to Expose
|
||||
|
||||
```prometheus
|
||||
# Service
|
||||
http_requests_total{method, path, status}
|
||||
http_request_duration_seconds{quantile="0.99"}
|
||||
errors_total{type}
|
||||
active_users
|
||||
|
||||
# Business
|
||||
orders_created_total{currency}
|
||||
revenue_total
|
||||
jobs_failed_total
|
||||
|
||||
# System
|
||||
process_cpu_seconds_total
|
||||
process_resident_memory_bytes
|
||||
process_open_fds
|
||||
```
|
||||
|
||||
## Distributed Tracing (Jaeger / OpenTelemetry)
|
||||
|
||||
- **Every request gets a `trace_id`** — propagate through headers (`x-trace-id`, `traceparent`).
|
||||
- **Span per operation** — HTTP handler, DB query, external API call.
|
||||
- **Annotate spans** — with relevant metadata (user ID, order ID, error details).
|
||||
- **Sampling:** sample 100% of errors, 1-10% of successful requests.
|
||||
- **Dapr/Jaeger:** auto-injects trace headers across sidecars.
|
||||
|
||||
```typescript
|
||||
// OpenTelemetry example
|
||||
const tracer = opentelemetry.trace.getTracer('order-service');
|
||||
const span = tracer.startSpan('createOrder', { attributes: { orderId } });
|
||||
try {
|
||||
const result = await createOrder(input);
|
||||
span.setStatus({ code: SpanStatusCode.OK });
|
||||
return result;
|
||||
} catch (e) {
|
||||
span.recordException(e);
|
||||
span.setStatus({ code: SpanStatusCode.ERROR });
|
||||
throw e;
|
||||
} finally {
|
||||
span.end();
|
||||
}
|
||||
```
|
||||
|
||||
## Alerting
|
||||
|
||||
### Alert Design
|
||||
|
||||
- **Alert on symptoms, not causes** — "API error rate >1%" not "server CPU >90%"
|
||||
- **Alert fatigue kills alerts** — every alert should need human action. If nobody acts, delete it.
|
||||
- **Define SLOs** — 99.9% uptime, 95% requests <500ms. Alert when approaching breach.
|
||||
- **Alert structure:**
|
||||
```
|
||||
Title: [P1] High error rate on order-service
|
||||
Summary: Error rate = 5.2% (threshold: 1%) over last 5 minutes
|
||||
Runbook: /runbooks/high-error-rate.md
|
||||
Severity: P1 (critical, paging) / P2 (urgent, business hours) / P3 (warning, ticket)
|
||||
```
|
||||
|
||||
### Common Alert Rules
|
||||
|
||||
| Rule | Severity | Threshold |
|
||||
|------|----------|-----------|
|
||||
| High error rate | P1 | >1% over 5min |
|
||||
| High latency | P2 | p99 >1s over 5min |
|
||||
| Service down | P1 | No metrics for 5min |
|
||||
| Disk space | P2 | <10% free |
|
||||
| Certificate expiring | P2 | <30 days |
|
||||
| Rate limit hit rate | P3 | >10% of requests rate-limited |
|
||||
|
||||
## Observability Stack (for this repo)
|
||||
|
||||
| Component | Role |
|
||||
|-----------|------|
|
||||
| **Prometheus** | Metrics collection + alerting |
|
||||
| **Jaeger** | Distributed tracing (all-in-one, OTLP receiver) |
|
||||
| **Traefik** | Metrics endpoint (`--metrics.prometheus=true`) |
|
||||
| **Docker labels** | Auto-discovery: `prometheus.io/scrape=true` |
|
||||
| **Dashboard** | `/dashboard` — auto-refresh 15s, shows containers + traces + metrics |
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- ❌ **String interpolation in logs** — `log.info("User " + id + " logged in")` instead of structured fields
|
||||
- ❌ **Logging in every function** — no, log at service boundaries and business events
|
||||
- ❌ **No correlation IDs** — cannot trace request across services
|
||||
- ❌ **Silent catch** — `catch (e) {}` swallows errors with no log
|
||||
- ❌ **Alert fatigue** — 50 alerts daily means none are actionable
|
||||
- ❌ **Dashboards without action** — beautiful dashboard but nobody knows what to do when a metric goes red
|
||||
@@ -0,0 +1,182 @@
|
||||
---
|
||||
name: monitoring
|
||||
description: Monitoring and observability best practices — Prometheus, Grafana, alerts, dashboards, uptime monitoring, and incident response. Use when setting up monitoring infrastructure, designing dashboards, defining alerts, or whenever the user mentions "monitoring," "Prometheus," "Grafana," "alert," "dashboard," "uptime," "incident," "runbook," "SLA," "SLO," "SLI," or "on-call."
|
||||
---
|
||||
|
||||
# Monitoring Best Practices
|
||||
|
||||
## Core Concepts
|
||||
|
||||
- **SLI** (Service Level Indicator) — what you measure (latency, error rate, uptime).
|
||||
- **SLO** (Service Level Objective) — target value (p99 < 500ms, error rate < 0.1%).
|
||||
- **SLA** (Service Level Agreement) — contractual commitment. Usually looser than SLO.
|
||||
|
||||
**Rule:** Set SLOs tighter than SLAs so you detect problems before customers do.
|
||||
|
||||
## Prometheus Setup (this repo)
|
||||
|
||||
```yaml
|
||||
# Prometheus auto-discovers containers with this label
|
||||
docker service create \
|
||||
--label prometheus.io/scrape=true \
|
||||
--label prometheus.io/path=/metrics \
|
||||
--label prometheus.io/port=8080 \
|
||||
...
|
||||
```
|
||||
|
||||
### Key Metrics to Export
|
||||
|
||||
Every service should expose a `/metrics` endpoint:
|
||||
|
||||
```prometheus
|
||||
# Service metrics
|
||||
http_requests_total{method="GET", path="/users", status="200"}
|
||||
http_request_duration_seconds{quantile="0.5", quantile="0.9", quantile="0.99"}
|
||||
http_requests_in_flight
|
||||
errors_total{type="validation", type="database", type="auth"}
|
||||
|
||||
# Traefik metrics (auto-exposed via `--metrics.prometheus=true`)
|
||||
traefik_service_request_duration_seconds{service="...", quantile="..."}
|
||||
traefik_service_requests_total{service="...", code="..."}
|
||||
|
||||
# System
|
||||
process_cpu_seconds_total
|
||||
process_resident_memory_bytes
|
||||
process_open_fds
|
||||
```
|
||||
|
||||
### Recording Rules
|
||||
```yaml
|
||||
groups:
|
||||
- name: service
|
||||
rules:
|
||||
- record: service:error_rate_5m
|
||||
expr: rate(errors_total[5m]) / rate(http_requests_total[5m])
|
||||
- record: service:latency_p99_5m
|
||||
expr: histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[5m]))
|
||||
```
|
||||
|
||||
## Grafana Dashboards
|
||||
|
||||
### Dashboard Components
|
||||
|
||||
| Panel | Metric | Good |
|
||||
|-------|--------|------|
|
||||
| **RPS** | `rate(http_requests_total[5m])` | Matches traffic patterns |
|
||||
| **Error rate** | `service:error_rate_5m` | < 1% |
|
||||
| **Latency** | `service:latency_p99_5m` | < 500ms |
|
||||
| **CPU** | `process_cpu_seconds_total` | < 80% sustained |
|
||||
| **Memory** | `process_resident_memory_bytes` | Steady, no leaks |
|
||||
| **Active connections** | `http_requests_in_flight` | < configured max |
|
||||
| **Open file descriptors** | `process_open_fds` | < 50% of limit |
|
||||
|
||||
### Dashboard Design Rules
|
||||
- **Single pane of glass** — most important metrics visible without scrolling.
|
||||
- **Red/yellow/green thresholds** — at a glance status.
|
||||
- **Time range controls** — last 15m, 1h, 6h, 1d, 7d.
|
||||
- **Template variables** — select by service, host, environment.
|
||||
- **Annotations** — mark deployments, config changes on timeline.
|
||||
|
||||
## Alerting Rules
|
||||
|
||||
```yaml
|
||||
groups:
|
||||
- name: alerts
|
||||
rules:
|
||||
- alert: HighErrorRate
|
||||
expr: service:error_rate_5m > 0.01
|
||||
for: 5m
|
||||
labels:
|
||||
severity: critical
|
||||
annotations:
|
||||
summary: "{{ $labels.service }} error rate is {{ $value | humanizePercentage }}"
|
||||
|
||||
- alert: HighLatency
|
||||
expr: service:latency_p99_5m > 1.0
|
||||
for: 5m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
summary: "{{ $labels.service }} p99 latency is {{ $value }}s"
|
||||
|
||||
- alert: ServiceDown
|
||||
expr: up{job="~.*"} == 0
|
||||
for: 1m
|
||||
labels:
|
||||
severity: critical
|
||||
annotations:
|
||||
summary: "{{ $labels.job }} has been down for >1 minute"
|
||||
```
|
||||
|
||||
### Severity Levels
|
||||
|
||||
| Level | Response | Time to Acknowledge |
|
||||
|-------|----------|---------------------|
|
||||
| **P1 (Critical)** | Pages on-call | 5 min |
|
||||
| **P2 (High)** | Alerts team during business hours | 30 min |
|
||||
| **P3 (Medium)** | Ticket, next business day | 8 hours |
|
||||
| **P4 (Low)** | Backlog, no deadline | N/A |
|
||||
|
||||
## Uptime Monitoring
|
||||
|
||||
- **Synthetic checks** — test critical user journeys every minute.
|
||||
- **SSL certificate expiry** — alert when <30 days remaining.
|
||||
- **Blackbox monitoring** — external service checking your endpoints.
|
||||
- **Heartbeat** — cron job pings a Dead Man's Switch — if it stops, on-call is paged.
|
||||
|
||||
## Incident Response
|
||||
|
||||
### Runbook Template
|
||||
```markdown
|
||||
# Runbook: High Error Rate
|
||||
|
||||
## Symptoms
|
||||
- >1% HTTP 5xx errors over 5 minutes
|
||||
- Slack alert in #monitoring
|
||||
|
||||
## 1. Check Traefik logs
|
||||
`docker logs traefik --tail 100 | grep "5[0-9][0-9]"`
|
||||
|
||||
## 2. Check service logs
|
||||
`docker logs <service> --tail 200`
|
||||
|
||||
## 3. Check database
|
||||
- Connection pool (Redis: `INFO clients`)
|
||||
- Query performance (`EXPLAIN ANALYZE` on slow queries)
|
||||
|
||||
## 4. Rollback if needed
|
||||
- Revert to previous stable image: `docker compose up -d <service>@sha256:<prev>`
|
||||
```
|
||||
|
||||
### IR Checklist
|
||||
1. **Acknowledge** — confirm you're investigating.
|
||||
2. **Mitigate** — stop the bleeding (rollback, disable feature flag, scale up).
|
||||
3. **Resolve** — apply the fix, verify metrics return to baseline.
|
||||
4. **Review** — postmortem (blameless). What happened? Why? How to prevent?
|
||||
|
||||
## Logging Integration
|
||||
|
||||
- **Structured logs** (JSON) indexed by Loki or ELK.
|
||||
- **Correlate logs with metrics** — trace_id in both.
|
||||
- **Error sampling** — capture 100% of errors, 1-10% of successful requests.
|
||||
|
||||
## This Repo's Monitoring Stack
|
||||
|
||||
| Component | Role |
|
||||
|-----------|------|
|
||||
| **Prometheus** | Metrics + alerting (Docker SD auto-discovery) |
|
||||
| **Jaeger** | Tracing (OTLP receiver, all-in-one) |
|
||||
| **Traefik** | Exposes metrics (--metrics.prometheus=true) |
|
||||
| **Docker labels** | `prometheus.io/scrape=true` for auto-discovery |
|
||||
| **Dashboard** | `/dashboard` (auto-refresh 15s) |
|
||||
| **Dashboard API** | `/api/dashboard` — JSON with containers, traces, metrics |
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- ❌ No SLOs — "everything should be fast" is not a target
|
||||
- ❌ Dashboard overload — metrics vomit with no narrative
|
||||
- ❌ Alert fatigue — 100 alerts per day means none are actionable
|
||||
- ❌ No runbooks — "what do I do when this alert fires?"
|
||||
- ❌ Only monitoring infrastructure — no business metrics (orders/min, signups)
|
||||
- ❌ Not monitoring after hours — 24/7 service needs 24/7 monitoring
|
||||
- ❌ No log retention policy — infinite logs = infinite cost
|
||||
@@ -0,0 +1,169 @@
|
||||
---
|
||||
name: monorepo
|
||||
description: Monorepo best practices — tooling, workspace configuration, shared dependencies, CI/CD, and dependency management. Use when working in monorepos (pnpm workspaces, moon, turborepo, Nx), managing shared packages, or whenever the user mentions "monorepo," "workspace," "pnpm workspace," "moon," "turborepo," "nx," "shared package," "dependency management," or "submodule."
|
||||
---
|
||||
|
||||
# Monorepo Best Practices
|
||||
|
||||
## Tool Selection
|
||||
|
||||
| Tool | Best For | Why |
|
||||
|------|----------|-----|
|
||||
| **pnpm workspaces** | Package management | Strict, fast, disk-efficient |
|
||||
| **moon** | Monorepo orchestration | Task orchestration + caching |
|
||||
| **turborepo** | Task orchestration | Simple caching, good for JS/TS |
|
||||
| **Nx** | Full monorepo framework | Generators, dependency graph, affected commands |
|
||||
| **Git submodules** | Multi-repo coordination | Separate repos imported together (this repo's pattern) |
|
||||
|
||||
## Workspace Structure (pnpm + moon)
|
||||
|
||||
```
|
||||
├── apps/
|
||||
│ ├── hub/ # Next.js app (submodule)
|
||||
│ └── scraper/ # Rust API (submodule)
|
||||
├── packages/ # Shared libraries (when not submodules)
|
||||
├── infra/ # Shared infra config
|
||||
├── pnpm-workspace.yaml
|
||||
├── moon.yml
|
||||
└── package.json
|
||||
```
|
||||
|
||||
### pnpm-workspace.yaml
|
||||
```yaml
|
||||
packages:
|
||||
- 'apps/*'
|
||||
- 'packages/*'
|
||||
```
|
||||
|
||||
### moon.yml (root)
|
||||
```yaml
|
||||
$schema: 'https://moonrepo.dev/schemas/project.json'
|
||||
language: 'typescript'
|
||||
type: 'application'
|
||||
```
|
||||
|
||||
## Shared Dependencies
|
||||
|
||||
```bash
|
||||
# Install a shared dependency
|
||||
pnpm add -w typescript
|
||||
|
||||
# Install in a specific package
|
||||
pnpm add --filter @scope/package zod
|
||||
|
||||
# Run in all packages
|
||||
pnpm -r run build
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
- **One version of a dependency across the monorepo** — use `pnpm overrides` or `resolution`.
|
||||
- **Root `devDependencies`** for shared tooling (TypeScript, Biome, ESLint).
|
||||
- **Explicit `dependencies`** — never rely on hoisting.
|
||||
- **Lock file** (`pnpm-lock.yaml`) committed — immutable installs.
|
||||
|
||||
## Git Submodules (this repo's pattern)
|
||||
|
||||
```
|
||||
asepharyana-hub/
|
||||
├── apps/
|
||||
│ ├── hub/ → asepharyana/asepharyana-hub-hub
|
||||
│ └── scraper/ → asepharyana/asepharyana-hub-scraper
|
||||
```
|
||||
|
||||
### Submodule Workflow
|
||||
```bash
|
||||
# Init after clone
|
||||
git submodule update --init --recursive
|
||||
|
||||
# Update all submodules to latest
|
||||
git submodule foreach git pull origin main
|
||||
|
||||
# Update one submodule
|
||||
cd apps/hub && git checkout main && git pull
|
||||
cd ../.. && git add apps/hub && git commit -m "chore(deps): update hub submodule"
|
||||
git push
|
||||
```
|
||||
|
||||
**State indicators:**
|
||||
- `(HEAD)` — detached at committed pointer (normal state).
|
||||
- `(main)` — on a branch (you've done `cd apps/name && git checkout main`).
|
||||
- Dirty — uncommitted changes inside submodule.
|
||||
|
||||
### When to Use Submodules vs Workspaces
|
||||
|
||||
| Need | Use |
|
||||
|------|-----|
|
||||
| Independent repos, separate deploy | Submodules |
|
||||
| Shared code within one repo | Workspaces |
|
||||
| Tightly coupled, always deploy together | Workspaces |
|
||||
| Loosely coupled, different teams | Submodules |
|
||||
|
||||
## CI/CD for Monorepos
|
||||
|
||||
### Selective Builds
|
||||
```yaml
|
||||
# Only run relevant workflows based on changed paths
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'apps/hub/**'
|
||||
- 'infra/docker/hub.Dockerfile'
|
||||
```
|
||||
|
||||
### Affected Commands (Nx/Turborepo/Moon)
|
||||
```bash
|
||||
moon ci # Runs affected tasks based on changes
|
||||
npx nx affected:test # Nx style
|
||||
turbo run build # Turborepo — leverages cache
|
||||
```
|
||||
|
||||
### Caching
|
||||
- **moon/turborepo** cache task outputs by file hash + env.
|
||||
- **pnpm** caches node_modules.
|
||||
- **Docker layer caching** — Registry-based caching for Docker builds.
|
||||
|
||||
## Shared Configuration
|
||||
|
||||
### TypeScript
|
||||
```jsonc
|
||||
// tsconfig.base.json at root — extended by all packages
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"moduleResolution": "bundler"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### ESLint / Biome
|
||||
```jsonc
|
||||
// biome.json at root — shared config for all packages
|
||||
{
|
||||
"formatter": { "indentStyle": "tab", "lineWidth": 120 },
|
||||
"linter": { "rules": { "recommended": true } }
|
||||
}
|
||||
```
|
||||
|
||||
## Dependency Management
|
||||
|
||||
- **Dependabot / Renovate** — automate dependency updates.
|
||||
- **`pnpm dedupe`** — deduplicate after updates.
|
||||
- **Check for duplicates** — `pnpm ls -r` or `pnpm why <package>`.
|
||||
- **When to upgrade:**
|
||||
- Patch: auto-merge.
|
||||
- Minor: update weekly.
|
||||
- Major: scheduled migration, document breaking changes.
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- ❌ Different dependency versions across packages — inconsistent builds
|
||||
- ❌ Hoisting assumptions — code works in dev but not in CI because of missing deps
|
||||
- ❌ Monolithic `package.json` — each package declares its own dependencies
|
||||
- ❌ No `.npmrc` with `shamefully-hoist=true` — defeats pnpm's strictness
|
||||
- ❌ Circular dependencies between packages — extract shared code
|
||||
- ❌ Every change rebuilds everything — use affected commands and caching
|
||||
- ❌ Submodule pointer drift — always commit after updating submodules
|
||||
@@ -0,0 +1,230 @@
|
||||
---
|
||||
name: nextjs
|
||||
description: Next.js App Router best practices — server components, client components, data fetching, routing, middleware, and deployment. Use when building Next.js applications, or whenever the user mentions "Next.js," "App Router," "server component," "client component," "SSR," "SSG," "ISR," "Middleware," "layout," "page," "route handler," "next/navigation," or "server actions."
|
||||
---
|
||||
|
||||
# Next.js Best Practices
|
||||
|
||||
## App Router Architecture
|
||||
|
||||
```
|
||||
app/
|
||||
├── (auth)/ # Route group — no URL segment
|
||||
│ ├── login/
|
||||
│ │ └── page.tsx
|
||||
│ └── register/
|
||||
│ └── page.tsx
|
||||
├── (dashboard)/
|
||||
│ ├── layout.tsx # Shared layout for all dashboard pages
|
||||
│ ├── page.tsx # /dashboard
|
||||
│ └── settings/
|
||||
│ └── page.tsx
|
||||
├── api/ # API route handlers
|
||||
│ └── users/
|
||||
│ └── route.ts
|
||||
├── layout.tsx # Root layout
|
||||
└── page.tsx # Home page (/)
|
||||
```
|
||||
|
||||
## Server Components (Default)
|
||||
|
||||
**Every component in App Router is a Server Component by default.** Use Client Components only when needed.
|
||||
|
||||
Server components can:
|
||||
- ✅ `async` component — `async function Page() { const data = await fetch(); ... }`
|
||||
- ✅ Direct database access — `await db.select().from(users)`
|
||||
- ✅ Import server-only modules (DB, filesystem, tokens)
|
||||
- ✅ `{children}` render client components
|
||||
|
||||
```typescript
|
||||
// ✅ Server Component — no "use client" directive
|
||||
export default async function UserPage({ params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params;
|
||||
const user = await db.select().from(users).where(eq(users.id, Number(id))).limit(1);
|
||||
if (!user[0]) return notFound();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>{user[0].name}</h1>
|
||||
<UserActions user={user[0]} /> {/* Client component */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Client Components — When to Use
|
||||
|
||||
Add `'use client'` only when you need:
|
||||
- 🔴 `useState` / `useReducer` — interactive UI state
|
||||
- 🔴 `useEffect` — browser-side effects or synchronization
|
||||
- 🔴 `useRouter` — programmatic navigation
|
||||
- 🔴 Event handlers — `onClick`, `onSubmit`, `onChange`
|
||||
- 🔴 Browser-only APIs — `localStorage`, `setInterval`, `IntersectionObserver`
|
||||
- 🔴 Custom hooks that use any of the above
|
||||
|
||||
```typescript
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
export function UserActions({ user }: { user: User }) {
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
return (
|
||||
<button onClick={() => setIsEditing(true)}>Edit {user.name}</button>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Data Fetching Patterns
|
||||
|
||||
### Server-side (preferred)
|
||||
```typescript
|
||||
// Direct DB access in server component — no waterfall, no loading states
|
||||
export default async function Dashboard() {
|
||||
const [stats, recentOrders, topUsers] = await Promise.all([
|
||||
getStats(), getRecentOrders(), getTopUsers(),
|
||||
]);
|
||||
return <DashboardView stats={stats} orders={recentOrders} users={topUsers} />;
|
||||
}
|
||||
```
|
||||
|
||||
### React Cache (deduplication)
|
||||
```typescript
|
||||
import { cache } from 'react';
|
||||
|
||||
export const getItem = cache(async (id: string) => {
|
||||
const item = await db.select().from(items).where(eq(items.id, id)).limit(1);
|
||||
return item[0];
|
||||
});
|
||||
```
|
||||
|
||||
### Revalidation
|
||||
```typescript
|
||||
// Time-based (ISR)
|
||||
export const revalidate = 3600; // seconds
|
||||
|
||||
// On-demand
|
||||
import { revalidatePath, revalidateTag } from 'next/cache';
|
||||
|
||||
export async function updateUser(formData: FormData) {
|
||||
'use server';
|
||||
await db.update(users).set({ name: formData.get('name') }).where(eq(users.id, id));
|
||||
revalidatePath(`/users/${id}`); // Revalidate the page
|
||||
revalidateTag('users'); // Revalidate all fetch calls with this tag
|
||||
}
|
||||
```
|
||||
|
||||
## Route Handlers (APIs)
|
||||
|
||||
```typescript
|
||||
// app/api/users/route.ts
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
|
||||
const CreateUserSchema = z.object({
|
||||
email: z.string().email(),
|
||||
});
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const body = await request.json();
|
||||
const parsed = CreateUserSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return NextResponse.json({ error: parsed.error.flatten() }, { status: 422 });
|
||||
}
|
||||
const user = await db.insert(users).values(parsed.data).returning();
|
||||
return NextResponse.json(user[0], { status: 201 });
|
||||
}
|
||||
```
|
||||
|
||||
## Server Actions
|
||||
|
||||
```typescript
|
||||
// app/users/actions.ts
|
||||
'use server';
|
||||
|
||||
import { z } from 'zod';
|
||||
import { db } from '@/db';
|
||||
import { users } from '@/db/schema';
|
||||
import { revalidatePath } from 'next/cache';
|
||||
|
||||
const CreateUserSchema = z.object({
|
||||
email: z.string().email(),
|
||||
name: z.string().min(1),
|
||||
});
|
||||
|
||||
export async function createUser(formData: FormData) {
|
||||
const parsed = CreateUserSchema.safeParse(Object.fromEntries(formData));
|
||||
if (!parsed.success) return { error: parsed.error.flatten() };
|
||||
|
||||
await db.insert(users).values(parsed.data);
|
||||
revalidatePath('/users');
|
||||
return { success: true };
|
||||
}
|
||||
```
|
||||
|
||||
## Middleware
|
||||
|
||||
```typescript
|
||||
// middleware.ts
|
||||
import { NextResponse } from 'next/server';
|
||||
import type { NextRequest } from 'next/server';
|
||||
|
||||
export function middleware(request: NextRequest) {
|
||||
const token = request.cookies.get('session')?.value;
|
||||
const { pathname } = request.nextUrl;
|
||||
|
||||
// Protected routes
|
||||
if (pathname.startsWith('/dashboard') && !token) {
|
||||
return NextResponse.redirect(new URL('/login', request.url));
|
||||
}
|
||||
|
||||
// Redirect logged-in users away from login
|
||||
if (pathname === '/login' && token) {
|
||||
return NextResponse.redirect(new URL('/dashboard', request.url));
|
||||
}
|
||||
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: ['/dashboard/:path*', '/login'],
|
||||
};
|
||||
```
|
||||
|
||||
## Performance
|
||||
|
||||
- **Server Components** over Client Components whenever possible.
|
||||
- **Streaming** — use `loading.tsx` and `Suspense` boundaries.
|
||||
- **Image optimization** — `next/image` with `priority` for above-the-fold.
|
||||
- **Font optimization** — `next/font` (self-hosted, no layout shift).
|
||||
- **Bundle analysis** — `@next/bundle-analyzer` for tracking bloat.
|
||||
- **Link prefetch** — `<Link>` prefetches by default. Disable with `prefetch={false}` for low-priority links.
|
||||
|
||||
## Testing
|
||||
|
||||
```typescript
|
||||
// Vitest + Testing Library (not Next.js's built-in jest-config)
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import Page from './page';
|
||||
|
||||
// Mock server component — render with test data
|
||||
vi.mock('@/db', () => ({ select: () => ({ from: () => ({ where: () => ({ limit: () => [mockUser] }) }) }) }));
|
||||
|
||||
describe('UserPage', () => {
|
||||
it('renders user name', async () => {
|
||||
const page = await Page({ params: Promise.resolve({ id: '1' }) });
|
||||
render(page);
|
||||
expect(screen.getByText('Alice')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- ❌ `'use client'` on every component — most components can be server components
|
||||
- ❌ Data fetching in client components — prefer server components for data
|
||||
- ❌ `useEffect` for data fetching — use server components or TanStack Query
|
||||
- ❌ Importing server-only code in client components (DB, tokens, env)
|
||||
- ❌ Large client bundles — lazy load heavy components with `next/dynamic`
|
||||
- ❌ Not using `notFound()` — always handle missing data
|
||||
- ❌ `router.push` for navigation that should use `<Link>` prefetch
|
||||
@@ -0,0 +1,123 @@
|
||||
---
|
||||
name: performance
|
||||
description: Best practices for software performance — caching, query optimization, lazy loading, profiling, CDN, database indexing, and memory management. Use when optimizing slow endpoints, reducing load times, designing caching strategies, or whenever the user mentions "performance," "optimization," "slow," "cache," "lazy loading," "profiling," "bottleneck," "N+1," "latency," "throughput," or "scalability."
|
||||
---
|
||||
|
||||
# Performance Best Practices
|
||||
|
||||
## Core Principle
|
||||
|
||||
**Measure before optimizing.** A guess is wrong more than half the time. Profile first, then fix the real bottleneck.
|
||||
|
||||
## Frontend Performance
|
||||
|
||||
### Loading
|
||||
- **Lazy load** — images, components, routes, heavy modules. Only what's needed now.
|
||||
- **Code splitting** — split by route (dynamic imports), not by random chunks.
|
||||
- **Preload critical assets** — `<link rel="preload">` for fonts, hero images, critical CSS.
|
||||
- **Prefetch likely navigations** — `<link rel="prefetch">` for pages user is likely to visit.
|
||||
|
||||
### Rendering
|
||||
- **Virtual lists** — for 100+ items. windowing (react-window, tanstack-virtual).
|
||||
- **Debounce/throttle** — search inputs (300ms debounce), scroll handlers (throttle 100ms).
|
||||
- **Avoid layout thrashing** — batch DOM reads/writes. Use `requestAnimationFrame`.
|
||||
- **CSS containment** — `contain: contents` isolates sub-trees from layout recalc.
|
||||
|
||||
### Assets
|
||||
- **Images** — next-gen formats (WebP, AVIF), responsive (`srcset`), lazy loading (`loading="lazy"`).
|
||||
- **Fonts** — `font-display: swap`, subset fonts, preload critical ones.
|
||||
- **Bundles** — tree-shaking enabled, minification, compression (brotli > gzip).
|
||||
|
||||
## Backend Performance
|
||||
|
||||
### Database
|
||||
|
||||
| Issue | Fix |
|
||||
|-------|-----|
|
||||
| **N+1 queries** | Eager loading (`.with()`, `.include()`, `JOIN`) |
|
||||
| **Missing index** | `EXPLAIN ANALYZE` to find sequential scans. Add indexes on `WHERE`/`JOIN`/`ORDER BY` columns |
|
||||
| **Too many rows** | Pagination, cursor-based, limit queries |
|
||||
| **Expensive joins** | Denormalize, materialized view, or caching layer |
|
||||
| **Large JSON fields** | Only select columns needed, not `SELECT *` |
|
||||
|
||||
```sql
|
||||
-- ❌ N+1
|
||||
for each order: SELECT * FROM items WHERE order_id = ?
|
||||
-- ✅ Eager load
|
||||
SELECT * FROM items WHERE order_id IN (?, ?, ?, ...)
|
||||
```
|
||||
|
||||
### Caching Strategy
|
||||
|
||||
```
|
||||
Request → CDN (static assets) → API Gateway → App Cache → DB
|
||||
```
|
||||
|
||||
| Layer | Cache | TTL | Invalidates |
|
||||
|-------|-------|-----|-------------|
|
||||
| **CDN** | Static assets, API responses | Long (1yr for assets) | Version hash |
|
||||
| **HTTP** | `Cache-Control`, ETag | Varies | `If-None-Match` |
|
||||
| **App** | Redis, in-memory | Seconds-minutes | Write-through / TTL |
|
||||
| **DB** | Query cache, connection pool | Intrinsic | Row changes |
|
||||
|
||||
**Cache patterns:**
|
||||
```typescript
|
||||
// Cache-aside (most common)
|
||||
async function getUser(id: string): Promise<User> {
|
||||
const cached = await cache.get(`user:${id}`);
|
||||
if (cached) return JSON.parse(cached);
|
||||
const user = await db.select().from(users).where(eq(users.id, id));
|
||||
await cache.set(`user:${id}`, JSON.stringify(user), 'EX', 300); // 5 min TTL
|
||||
return user;
|
||||
}
|
||||
```
|
||||
|
||||
### Connection Pooling
|
||||
- **Database:** pool of 10-50 connections (not 1, not unlimited).
|
||||
- **HTTP:** keep-alive, connection reuse. H2 multiplexing.
|
||||
- **Redis:** single connection reused, not new connection per request.
|
||||
|
||||
## Network Performance
|
||||
|
||||
- **Compression** — brotli for static, gzip as fallback. Enable in Traefik (`compress` middleware).
|
||||
- **HTTP/2** — multiplexing, header compression, server push. Enabled by default in Traefik.
|
||||
- **CDN** — CloudFlare, Fastly, CloudFront for static assets and API edge caching.
|
||||
- **Keep-alive** — reuse TCP connections. Default Timeout 60s.
|
||||
- **Latency budget** — 200ms total is good for most apps. Track per service.
|
||||
|
||||
## Profiling
|
||||
|
||||
### When you think something is slow:
|
||||
1. **Define the measurement** — what's slow? p50? p99? cold start?
|
||||
2. **Profile** — flame graphs (pyroscope, pprof), APM (Jaeger spans).
|
||||
3. **Find the bottleneck** — is it CPU? IO? Network? Database? Memory?
|
||||
4. **Fix one thing** — measure again. If no improvement, revert and try next.
|
||||
|
||||
### Tools by Language
|
||||
|
||||
| Language | Profiling | Flame Graphs |
|
||||
|----------|-----------|--------------|
|
||||
| TypeScript | Chrome DevTools, Node `--prof` | `0x` tool |
|
||||
| Rust | `perf`, `flamegraph`, `pprof-rs` | `cargo flamegraph` |
|
||||
| Go | `pprof` (runtime built-in) | `go tool pprof -http` |
|
||||
| Python | `cProfile`, `py-spy` | `flameprof` |
|
||||
|
||||
## Performance Budgets
|
||||
|
||||
Set measurable limits and enforce them:
|
||||
- **Lighthouse** — 90+ Performance score
|
||||
- **Bundle size** — <200KB JS (compressed), <50KB CSS
|
||||
- **LCP** (Largest Contentful Paint) — <2.5s
|
||||
- **FID** (First Input Delay) — <100ms
|
||||
- **CLS** (Cumulative Layout Shift) — <0.1
|
||||
- **API p99** — <500ms
|
||||
- **First byte** — <200ms
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- ❌ **Premature optimization** — optimizing before measuring. "Make it work, make it right, make it fast."
|
||||
- ❌ **Caching everything** — cache invalidation is hard. Cache what's expensive and stable.
|
||||
- ❌ **Over-indexing** — too many indexes slow writes. Index what's queried, not every column.
|
||||
- ❌ **SELECT *** — fetches columns you don't need. Increases memory and network.
|
||||
- ❌ **Sync over async** — blocking calls in async context (Node event loop blocking).
|
||||
- ❌ **Fat dependencies** — importing a 50KB library for one function. Prefer tree-shakeable modules.
|
||||
@@ -0,0 +1,172 @@
|
||||
---
|
||||
name: python
|
||||
description: Python best practices — typing, project structure, packaging, FastAPI patterns, async, testing, and idiomatic Python. Use when writing Python code, structuring a Python project, or whenever the user mentions "Python," "FastAPI," "Django," "pytest," "PEP 8," "type hints," "Pydantic," "asyncio," "pip," "poetry," or "uv."
|
||||
---
|
||||
|
||||
# Python Best Practices
|
||||
|
||||
## Type Hints
|
||||
|
||||
Python 3.10+ type hints are the standard. Enable via `pyproject.toml`.
|
||||
|
||||
```toml
|
||||
[tool.pyright]
|
||||
typeCheckingMode = "strict"
|
||||
```
|
||||
|
||||
```python
|
||||
from collections.abc import Sequence
|
||||
from dataclasses import dataclass
|
||||
from typing import assert_never
|
||||
|
||||
@dataclass
|
||||
class User:
|
||||
id: str
|
||||
email: str
|
||||
name: str | None # Optional[str] in 3.8-3.9
|
||||
|
||||
def create_user(
|
||||
email: str,
|
||||
name: str | None = None,
|
||||
tags: Sequence[str] = (),
|
||||
) -> User: ...
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
- Annotate all function signatures (params + return). No `def f(x, y):`.
|
||||
- Use `|` union syntax (3.10+) over `Optional[Union[...]]`.
|
||||
- Use `Sequence` over `List` for parameters (accepts tuples/lists/sets).
|
||||
- Use `TypeVar` for generics.
|
||||
- Avoid `Any`. Use `object` or `Unknown` (pyright) if truly untyped.
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
project/
|
||||
├── src/
|
||||
│ └── app/
|
||||
│ ├── __init__.py
|
||||
│ ├── domain/ # Pure business logic
|
||||
│ ├── application/ # Use cases
|
||||
│ ├── infrastructure/ # DB, external APIs
|
||||
│ └── presentation/ # API routes, CLI
|
||||
├── tests/
|
||||
│ ├── unit/
|
||||
│ └── integration/
|
||||
├── pyproject.toml
|
||||
├── uv.lock # or poetry.lock
|
||||
└── README.md
|
||||
```
|
||||
|
||||
**Packaging:** Always use `src/` layout — prevents importing from project root by accident.
|
||||
|
||||
## Modern Tooling
|
||||
|
||||
| Tool | Purpose | Overrides |
|
||||
|------|---------|-----------|
|
||||
| **uv** | Package manager, venv, runner | pip, poetry, pipenv |
|
||||
| **pytest** | Testing | unittest |
|
||||
| **ruff** | Linter + formatter | flake8, black, isort |
|
||||
| **pyright** | Type checker | mypy |
|
||||
| **Pydantic** | Validation + serialization | dataclasses (for complex validation) |
|
||||
|
||||
```toml
|
||||
[tool.ruff]
|
||||
target-version = "py312"
|
||||
line-length = 100
|
||||
select = ["E", "F", "I", "N", "W", "UP", "B", "SIM", "ARG", "RUF"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
```
|
||||
|
||||
## FastAPI Patterns
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel, EmailStr
|
||||
from fastapi import FastAPI, HTTPException
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# Pydantic models at the boundary
|
||||
class CreateUserRequest(BaseModel):
|
||||
email: EmailStr
|
||||
name: str | None = None
|
||||
|
||||
class UserResponse(BaseModel):
|
||||
id: str
|
||||
email: str
|
||||
name: str | None
|
||||
|
||||
@app.post("/users", response_model=UserResponse, status_code=201)
|
||||
async def create_user(body: CreateUserRequest):
|
||||
# Use case or service layer — not raw ORM here
|
||||
user = await user_service.create(body.email, body.name)
|
||||
if not user:
|
||||
raise HTTPException(409, "Email already exists")
|
||||
return UserResponse.model_validate(user)
|
||||
```
|
||||
|
||||
- **Dependency injection** — FastAPI `Depends()` for shared deps (DB session, auth).
|
||||
- **Pydantic v2** — `model_validate()` not `from_orm()`.
|
||||
- **Path operations** — thin controllers. Business logic in use cases.
|
||||
|
||||
## Async Best Practices
|
||||
|
||||
- **Only async when you need IO** — DB, HTTP, file, network. CPU work should stay sync.
|
||||
- **Use `asyncio.run()`** for entry point, `asyncio.gather()` for concurrent IO.
|
||||
- **Never mix blocking with async** — no `time.sleep()`, no `requests` in async code.
|
||||
- **Prefer `httpx.AsyncClient`** over `requests` for async APIs.
|
||||
- **Database:** `asyncpg` (Postgres), `redis.asyncio`, `motor` (Mongo).
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
import httpx
|
||||
|
||||
async def fetch_all(urls: list[str]) -> list[dict]:
|
||||
async with httpx.AsyncClient() as client:
|
||||
tasks = [client.get(url) for url in urls]
|
||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
return [r.json() for r in results if isinstance(r, httpx.Response)]
|
||||
```
|
||||
|
||||
## Testing (pytest)
|
||||
|
||||
```python
|
||||
# tests/unit/test_order.py
|
||||
from app.domain.order import Order, OrderItem
|
||||
from datetime import datetime
|
||||
|
||||
def test_order_total_calculates_correctly():
|
||||
order = Order(items=[
|
||||
OrderItem(price=10.0, quantity=2),
|
||||
OrderItem(price=5.0, quantity=1),
|
||||
])
|
||||
assert order.total == 25.0 # 10*2 + 5*1
|
||||
|
||||
def test_order_rejects_empty_items():
|
||||
with pytest.raises(ValueError, match="at least one item"):
|
||||
Order(items=[])
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_user_duplicate_email():
|
||||
repo = InMemoryUserRepo() # fake
|
||||
service = UserService(repo)
|
||||
await service.create("a@x.com")
|
||||
with pytest.raises(DuplicateEmailError):
|
||||
await service.create("a@x.com")
|
||||
```
|
||||
|
||||
- **Fixtures over setup/teardown.**
|
||||
- **Parametrize** for multiple cases — `@pytest.mark.parametrize`.
|
||||
- **Fakes over mocks** — in-memory DB, fake HTTP client.
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- ❌ `from module import *` — pollutes namespace
|
||||
- ❌ Mutable default args — `def f(x=[]):` — shared across calls
|
||||
- ❌ Bare `except:` — catches `KeyboardInterrupt`, `SystemExit`, everything
|
||||
- ❌ Type hints at wrong level — only annotate public API, not every internal variable
|
||||
- ❌ `print()` for debugging — use `logging` or `loguru`
|
||||
- ❌ `requirements.txt` without lock — use `uv.lock` / `poetry.lock`
|
||||
- ❌ Try-except-pass — swallows errors silently
|
||||
@@ -0,0 +1,195 @@
|
||||
---
|
||||
name: react-frontend
|
||||
description: React and frontend best practices — component patterns, hooks, state management, TanStack Query, React Router, performance, and testing. Use when building React components, designing state management, or whenever the user mentions "React," "hooks," "state management," "component," "JSX," "TanStack Query," "React Router," "Zustand," "Vite," "Next.js," or "Frontend."
|
||||
---
|
||||
|
||||
# React Frontend Best Practices
|
||||
|
||||
## Component Patterns
|
||||
|
||||
### Composition over Inheritance
|
||||
```typescript
|
||||
// ✅ Prefer composition
|
||||
function Layout({ sidebar, children }: { sidebar: ReactNode; children: ReactNode }) {
|
||||
return <div className="layout">{sidebar}<main>{children}</main></div>;
|
||||
}
|
||||
|
||||
// ❌ Avoid inheritance patterns in React
|
||||
```
|
||||
|
||||
### Container/Presentational Separation
|
||||
```typescript
|
||||
// Container: manages state, data fetching, business logic
|
||||
function UserProfileContainer() {
|
||||
const { data: user } = useUserQuery(userId);
|
||||
const { mutate: update } = useUpdateUserMutation();
|
||||
return <UserProfile user={user!} onUpdate={update} />;
|
||||
}
|
||||
|
||||
// Presentational: pure rendering, props-only
|
||||
function UserProfile({ user, onUpdate }: UserProfileProps) {
|
||||
return <div>{user.name} <button onClick={onUpdate}>Edit</button></div>;
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Hooks for Logic Extraction
|
||||
```typescript
|
||||
// Extract reusable logic into custom hooks
|
||||
function useUserPermissions(userId: string) {
|
||||
const { data: user } = useUserQuery(userId);
|
||||
return useMemo(() => ({
|
||||
isAdmin: user?.role === 'admin',
|
||||
canEdit: user?.role === 'admin' || user?.role === 'editor',
|
||||
canDelete: user?.role === 'admin',
|
||||
}), [user]);
|
||||
}
|
||||
```
|
||||
|
||||
## Hooks Rules
|
||||
|
||||
- **Only call hooks at the top level** — not in conditions, loops, or callbacks.
|
||||
- **Only call hooks from React functions** — component or custom hook.
|
||||
- **Deps array matches reality** — include all values used inside.
|
||||
- **`useMemo`** for expensive computations. **`useCallback`** for stable references.
|
||||
- **`useEffect`** is for synchronization, not lifecycle. If you can compute from state, do it.
|
||||
|
||||
```typescript
|
||||
// ❌ Unnecessary effect
|
||||
const [fullName, setFullName] = useState('');
|
||||
useEffect(() => { setFullName(`${first} ${last}`); }, [first, last]);
|
||||
|
||||
// ✅ Derived state
|
||||
const fullName = `${first} ${last}`;
|
||||
```
|
||||
|
||||
## Data Fetching (TanStack Query)
|
||||
|
||||
```typescript
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
// Query
|
||||
function useUserQuery(id: string) {
|
||||
return useQuery({
|
||||
queryKey: ['users', id],
|
||||
queryFn: () => api.users.get({ params: { id } }),
|
||||
staleTime: 30_000, // 30s before refetch
|
||||
gcTime: 5 * 60_000, // 5min cache
|
||||
});
|
||||
}
|
||||
|
||||
// Mutation with optimistic update
|
||||
function useUpdateUserMutation() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (data: UpdateUserInput) => api.users.update({ body: data }),
|
||||
onMutate: async (newUser) => {
|
||||
await queryClient.cancelQueries({ queryKey: ['users', newUser.id] });
|
||||
const previous = queryClient.getQueryData(['users', newUser.id]);
|
||||
queryClient.setQueryData(['users', newUser.id], newUser);
|
||||
return { previous };
|
||||
},
|
||||
onError: (_, __, context) => {
|
||||
queryClient.setQueryData(['users', context.previous.id], context.previous);
|
||||
},
|
||||
onSettled: () => queryClient.invalidateQueries({ queryKey: ['users'] }),
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
- `staleTime` for read-through caching. Default 0 = refetch on mount.
|
||||
- `gcTime` for garbage collection of unused data.
|
||||
- `onMutate`/`onError`/`onSettled` for optimistic updates.
|
||||
- Queries over custom fetch + useEffect in all cases.
|
||||
|
||||
## State Management Selection
|
||||
|
||||
| Need | Solution |
|
||||
|------|----------|
|
||||
| Server state | TanStack Query |
|
||||
| URL state | React Router / TanStack Router |
|
||||
| Form state | React Hook Form + Zod |
|
||||
| Client state (global) | Zustand or Context |
|
||||
| Client state (local) | `useState` / `useReducer` |
|
||||
| Component communication | Props / lifting state up |
|
||||
|
||||
```typescript
|
||||
// Zustand — lightweight, no boilerplate
|
||||
import { create } from 'zustand';
|
||||
|
||||
interface UIStore {
|
||||
sidebarOpen: boolean;
|
||||
toggleSidebar: () => void;
|
||||
}
|
||||
const useUIStore = create<UIStore>((set) => ({
|
||||
sidebarOpen: true,
|
||||
toggleSidebar: () => set((s) => ({ sidebarOpen: !s.sidebarOpen })),
|
||||
}));
|
||||
```
|
||||
|
||||
## Routing (TanStack Router / React Router)
|
||||
|
||||
```typescript
|
||||
// TanStack Router — type-safe, modern
|
||||
const router = createRouter({
|
||||
routeTree: rootRoute.addChildren([
|
||||
indexRoute,
|
||||
usersRoute.addChildren([userRoute, userProfileRoute]),
|
||||
]),
|
||||
});
|
||||
```
|
||||
|
||||
- **File-based routing** (Next.js App Router, Vite/Router) for simpler projects.
|
||||
- **Type-safe routers** (TanStack Router) for larger apps.
|
||||
- **Lazy load** route components — `React.lazy(() => import('./routes/Dashboard'))`.
|
||||
|
||||
## Performance
|
||||
|
||||
- **Virtual lists** — `@tanstack/react-virtual` for 100+ items.
|
||||
- **React.memo** sparingly — only for components that re-render often with same props.
|
||||
- **`useMemo` for expensive calculations** — not for every value.
|
||||
- **Code splitting** — per route, per heavy component.
|
||||
- **Bundle analysis** — `vite-bundle-visualizer` to find bloat.
|
||||
|
||||
## Testing (Vitest + Testing Library)
|
||||
|
||||
```typescript
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { UserProfile } from './UserProfile';
|
||||
|
||||
describe('UserProfile', () => {
|
||||
it('renders user name', () => {
|
||||
render(<UserProfile user={{ name: 'Alice' }} />);
|
||||
expect(screen.getByText('Alice')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls onUpdate when edit clicked', async () => {
|
||||
const onUpdate = vi.fn();
|
||||
render(<UserProfile user={{ name: 'Alice' }} onUpdate={onUpdate} />);
|
||||
await userEvent.click(screen.getByRole('button', { name: /edit/i }));
|
||||
expect(onUpdate).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- **Testing Library** for user-centric tests. Never test implementation details.
|
||||
- **userEvent** over `fireEvent` — simulates real user interactions.
|
||||
- **Component-level** tests for behavior, not storybook-style visual tests here.
|
||||
|
||||
## CSS / Styling
|
||||
|
||||
- **Tailwind CSS** for utility-first styling. Consistent, fast, small.
|
||||
- **CSS Modules** when you need scoped component styles.
|
||||
- **CSS-in-JS** (styled-components, emotion) — only for dynamic theming. Prefer Tailwind.
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- ❌ `useEffect` for data fetching — use TanStack Query
|
||||
- ❌ Prop drilling beyond 3 levels — compose or context
|
||||
- ❌ `useState` for derived data — compute from existing state
|
||||
- ❌ Direct DOM manipulation — use React refs
|
||||
- ❌ `any` in component props — always type props
|
||||
- ❌ Large component files — split by responsibility
|
||||
- ❌ `index` as key — breaks reconciliation on reorder
|
||||
- ❌ `useEffect` without deps — runs every render
|
||||
@@ -0,0 +1,198 @@
|
||||
---
|
||||
name: rust
|
||||
description: Rust best practices — ownership, error handling, async, project structure, clippy rules, testing, and idiomatic Rust. Use when writing Rust code, reviewing Rust projects, or whenever the user mentions "Rust," "cargo," "clippy," "ownership," "borrowing," "lifetimes," "Result," "Option," "async/await," "tokio," "Axum," "SeaORM," or "unsafe."
|
||||
---
|
||||
|
||||
# Rust Best Practices
|
||||
|
||||
## Project Structure (Clean Architecture)
|
||||
|
||||
```
|
||||
src/
|
||||
├── domain/ # Entities, value objects, domain events
|
||||
├── application/ # Use cases, repository ports (traits)
|
||||
├── infrastructure/ # Adapters (DB, HTTP client, cache)
|
||||
├── api/ # Axum/Actix handlers, middleware
|
||||
├── config/ # App configuration
|
||||
└── main.rs # Entry point, composition root
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
```rust
|
||||
// Use thiserror for application errors
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum OrderError {
|
||||
#[error("order not found: {0}")]
|
||||
NotFound(String),
|
||||
#[error("validation error: {0}")]
|
||||
Validation(String),
|
||||
#[error("database error: {0}")]
|
||||
Database(#[from] sqlx::Error),
|
||||
}
|
||||
|
||||
// Use Result in application layer
|
||||
pub async fn create_order(input: CreateOrderInput) -> Result<Order, OrderError> {
|
||||
let user = user_repo.find_by_id(&input.user_id)
|
||||
.await?
|
||||
.ok_or_else(|| OrderError::NotFound("user".into()))?;
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
- `thiserror` for library/app errors. `anyhow` for binary/CLI errors.
|
||||
- Never `unwrap()` or `expect()` in production code. Use `?` or match.
|
||||
- `Result` for recoverable errors. `panic!` only for unrecoverable states.
|
||||
|
||||
## Ownership & Borrowing
|
||||
|
||||
```rust
|
||||
// Prefer borrowing over taking ownership
|
||||
fn process(items: &[Item]) -> usize {
|
||||
items.iter().filter(|i| i.active).count()
|
||||
}
|
||||
|
||||
// Clone when ownership is truly needed (and it actually needs to be owned)
|
||||
fn save(items: Vec<Item>) { ... }
|
||||
```
|
||||
|
||||
- **One mutable reference (`&mut`) XOR many immutable refs (`&`).**
|
||||
- **Use `Cow`** for "borrow if possible, own if modified."
|
||||
- **Prefer `&str`** over `&String` for function params.
|
||||
- **Rc/Arc** — only when shared ownership is needed (graphs, caches).
|
||||
|
||||
## Async (Tokio)
|
||||
|
||||
```rust
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
// ...
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Use tokio::spawn for concurrent tasks
|
||||
let handle = tokio::spawn(async move {
|
||||
process_batch(items).await
|
||||
});
|
||||
let result = handle.await??;
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
- `tokio` for runtime. Axum for HTTP.
|
||||
- `tokio::select!` for timeouts, race conditions.
|
||||
- No `block_on` in async code. No sync mutex in async — use `tokio::sync::Mutex`.
|
||||
- Use `tokio::sync::Semaphore` for rate limiting concurrent tasks.
|
||||
|
||||
## Traits (Interfaces)
|
||||
|
||||
```rust
|
||||
// Port — defined in domain/application layer
|
||||
#[async_trait]
|
||||
pub trait UserRepository: Send + Sync {
|
||||
async fn find_by_id(&self, id: &str) -> Result<Option<User>, DbError>;
|
||||
async fn save(&self, user: &User) -> Result<(), DbError>;
|
||||
}
|
||||
|
||||
// Adapter — implemented in infrastructure layer
|
||||
pub struct PostgresUserRepository {
|
||||
pool: sqlx::PgPool,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl UserRepository for PostgresUserRepository {
|
||||
async fn find_by_id(&self, id: &str) -> Result<Option<User>, DbError> {
|
||||
sqlx::query_as("SELECT * FROM users WHERE id = $1")
|
||||
.bind(id)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Database (sqlx / SeaORM)
|
||||
|
||||
```rust
|
||||
// sqlx — prefer raw SQL with compile-time checking
|
||||
let user = sqlx::query_as::<_, User>(
|
||||
"SELECT id, email, name FROM users WHERE id = $1"
|
||||
)
|
||||
.bind(user_id)
|
||||
.fetch_optional(&pool)
|
||||
.await?;
|
||||
```
|
||||
|
||||
- **sqlx** for type-safe raw SQL. **SeaORM** when you need a full ORM.
|
||||
- Use migrations (`sqlx migrate` or `sea-orm-cli`).
|
||||
- Connection pooling via `sqlx::PgPool` or `deadpool`.
|
||||
|
||||
## Testing
|
||||
|
||||
```rust
|
||||
// Unit tests inline
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn order_total_calculates_correctly() {
|
||||
let order = Order::new(vec![
|
||||
OrderItem::new(10.0, 2),
|
||||
OrderItem::new(5.0, 1),
|
||||
]);
|
||||
assert_eq!(order.total(), 25.0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_order_requires_user() {
|
||||
let repo = InMemoryUserRepo::new();
|
||||
let result = create_order(CreateOrderInput { user_id: "nonexistent".into() }, &repo).await;
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
|
||||
// Integration tests in tests/
|
||||
```
|
||||
|
||||
- **Unit tests** inline in module. **Integration** in `tests/` directory.
|
||||
- **Fakes** (in-memory repos) over mocking libraries.
|
||||
- **Property-based testing** with `proptest` for complex logic.
|
||||
|
||||
## Clippy Rules
|
||||
|
||||
```toml
|
||||
# .clippy.toml
|
||||
# Enable in Cargo.toml or rustfmt.toml:
|
||||
# [lints.clippy]
|
||||
# pedantic = "warn"
|
||||
# nursery = "warn"
|
||||
```
|
||||
|
||||
Default: `#![warn(clippy::pedantic, clippy::nursery)]`
|
||||
|
||||
## Serialization (serde)
|
||||
|
||||
```rust
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct UserResponse {
|
||||
pub id: String,
|
||||
pub email: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub name: Option<String>,
|
||||
}
|
||||
```
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- ❌ `unwrap()` / `expect()` in production — crash on any error
|
||||
- ❌ Large `main.rs` — everything in one file
|
||||
- ❌ `unsafe` without documented safety invariants
|
||||
- ❌ `Rc<RefCell<...>>` in async contexts — use `Arc<Mutex<...>>`
|
||||
- ❌ `Box<dyn Trait>` where generics work — `impl Trait` or generic param
|
||||
- ❌ Ignoring clippy warnings — run `clippy` before every commit
|
||||
- ❌ `#[tokio::main]` on library code — only in binary entry points
|
||||
@@ -0,0 +1,129 @@
|
||||
---
|
||||
name: security
|
||||
description: Security best practices for software development — input validation, authentication, authorization, secrets management, OWASP Top 10, and secure coding patterns. Use when handling user input, designing auth flows, storing secrets, configuring CORS/headers, or whenever the user mentions "security," "XSS," "SQL injection," "CSRF," "authentication," "authorization," "JWT," "OAuth," "encryption," "secrets," "CORS," "RBAC," "OWASP," or "security review."
|
||||
---
|
||||
|
||||
# Security Best Practices
|
||||
|
||||
## OWASP Top 10 (Quick Reference)
|
||||
|
||||
1. **Broken Access Control** — verify authorization for every action, not just login.
|
||||
2. **Cryptographic Failures** — use modern crypto (AES-256-GCM, bcrypt/Argon2 for passwords, TLS 1.3).
|
||||
3. **Injection** — never concatenate user input into SQL/shell/HTML. Use parameterized queries.
|
||||
4. **Insecure Design** — threat model before building. Rate limit, throttle, validate.
|
||||
5. **Security Misconfiguration** — remove defaults, disable debug in prod, use secure headers.
|
||||
6. **Vulnerable Components** — keep dependencies updated. Use SCA tools (Dependabot, Renovate).
|
||||
7. **Auth Failures** — MFA, rate-limit login, no weak passwords, secure session management.
|
||||
8. **Data Integrity Failures** — signed JWTs, integrity checks on CI/CD pipeline.
|
||||
9. **Logging Failures** — log auth failures, never log secrets, monitor suspicious patterns.
|
||||
10. **SSRF** — validate URLs, restrict outbound network access.
|
||||
|
||||
## Input Validation (First Line of Defense)
|
||||
|
||||
- **Whitelist > blacklist** — define what's allowed, not what's blocked.
|
||||
- **Validate at every trust boundary** — API gateway → service → use case.
|
||||
- **Type coercion** — parse, cast, then use. Never trust raw strings.
|
||||
- **Size limits** — enforce max lengths, max file sizes.
|
||||
- **Content types** — validate `Content-Type` and reject unexpected formats.
|
||||
|
||||
## Authentication
|
||||
|
||||
- **Passwords:** bcrypt (cost ≥10), Argon2id, or PBKDF2. Never MD5/SHA1.
|
||||
- **JWTs:** Use `jose`/`jsonwebtoken` with RS256 or ES256. Short TTL (15min access, 7d refresh max). Validate `aud`, `iss`, `exp`, `nbf`.
|
||||
- **Sessions:** HttpOnly, Secure, SameSite=Strict cookies. Rotate on privilege change.
|
||||
- **MFA:** Prefer TOTP/WebAuthn over SMS (SIM swap risk).
|
||||
- **Rate limiting:** Login/registration/password-reset endpoints. 5 attempts/15min per IP is standard.
|
||||
|
||||
## Authorization
|
||||
|
||||
- **RBAC** — roles assigned to users, permissions assigned to roles.
|
||||
- **ABAC** (attribute-based) — for fine-grained: resource owner, department, region.
|
||||
- **Check every request** — authorization at every endpoint, not just at login.
|
||||
- **Default deny** — fail closed. If no rule allows it, deny it.
|
||||
- **Middleware pattern:**
|
||||
```typescript
|
||||
// Authenticate → Authorize → Execute
|
||||
app.use('/api/*', authenticate);
|
||||
app.use('/api/admin/*', authorize('admin'));
|
||||
app.post('/api/orders', authorizeOrderAccess);
|
||||
```
|
||||
|
||||
## Secrets Management
|
||||
|
||||
- **Never commit secrets.** Use environment variables or a vault (Vault, AWS Secrets Manager, 1Password CLI).
|
||||
- **.env files** — never committed to git. Use `.env.example` as a template.
|
||||
- **Scan for secrets** — use `trufflehog`, `git-secrets`, or GitHub secret scanning.
|
||||
- **Rotate regularly** — API keys, DB passwords, JWT signing keys.
|
||||
- **Principle of least privilege** — tokens/secrets should have minimal scope.
|
||||
|
||||
## API Security
|
||||
|
||||
### Headers (via Traefik or middleware)
|
||||
```yaml
|
||||
# Traefik example
|
||||
middleware:
|
||||
secure-headers:
|
||||
headers:
|
||||
frameDeny: true
|
||||
contentTypeNosniff: true
|
||||
browserXssFilter: true
|
||||
sslRedirect: true
|
||||
referrerPolicy: "no-referrer-when-downgrade"
|
||||
permissionsPolicy: "camera=(), microphone=(), geolocation=()"
|
||||
customFrameOptionsValue: "SAMEORIGIN"
|
||||
contentSecurityPolicy: "default-src 'self'; script-src 'self'"
|
||||
```
|
||||
|
||||
### CORS
|
||||
- **Default:** same-origin only.
|
||||
- **For APIs:** whitelist specific origins, never `Access-Control-Allow-Origin: *` for authenticated endpoints.
|
||||
- **Credentials:** `Access-Control-Allow-Credentials: true` only with explicit origin.
|
||||
|
||||
### Rate Limiting
|
||||
- **Per IP, per user, per endpoint.** Different limits for different tiers.
|
||||
- **Return `429 Too Many Requests`** with `Retry-After` header.
|
||||
- **Log rate limit hits** — they often precede attacks.
|
||||
|
||||
## Data Protection
|
||||
|
||||
- **Encrypt at rest** — AES-256-GCM for PII. Transparent encryption or app-level.
|
||||
- **Encrypt in transit** — TLS 1.3 minimum. HSTS with `max-age=63072000`.
|
||||
- **PII minimization** — don't collect what you don't need. Anonymize when possible.
|
||||
- **Data retention** — delete old data. Have a purge policy.
|
||||
- **SQL injection prevention:**
|
||||
```typescript
|
||||
// ❌ Never
|
||||
db.execute(`SELECT * FROM users WHERE id = '${id}'`);
|
||||
// ✅ Always
|
||||
db.execute('SELECT * FROM users WHERE id = $1', [id]);
|
||||
```
|
||||
|
||||
## XSS Prevention
|
||||
|
||||
- **React/Vue/Svelte/Solid** — auto-escaped by default. Avoid `dangerouslySetInnerHTML`/`v-html`.
|
||||
- **CSP headers** — `Content-Security-Policy` restricts script sources.
|
||||
- **Never eval user input** — `JSON.parse` is safe, `eval` is not.
|
||||
- **Sanitize HTML** — use DOMPurify if you must render user HTML.
|
||||
|
||||
## CSRF Prevention
|
||||
|
||||
- **SameSite cookies** — `SameSite=Strict`/`Lax` covers most cases.
|
||||
- **CSRF tokens** — for forms without SameSite support.
|
||||
- **Custom headers** — `X-Requested-By` is a valid CSRF token pattern for APIs.
|
||||
|
||||
## Dependency Security
|
||||
|
||||
- **Regular audits** — `npm audit`, `cargo audit`, `pip-audit`.
|
||||
- **Lock files** — commit `package-lock.json`, `Cargo.lock`, `poetry.lock`.
|
||||
- **Dependabot/Renovate** — automate dependency updates.
|
||||
- **No `*` ranges** — pin major/minor, allow patches.
|
||||
- **SBOM** — generate Software Bill of Materials for production deployments.
|
||||
|
||||
## Language-Specific
|
||||
|
||||
| Language | Key Practice |
|
||||
|----------|-------------|
|
||||
| TypeScript | `strict: true`, no `any`, use `jose` for JWT |
|
||||
| Python | Use `httpx`/`requests` with TLS verify. Pydantic for validation |
|
||||
| Rust | `cargo audit`, `ring` for crypto, parameterized queries via sqlx |
|
||||
| Go | `crypto` stdlib, parameterized with `database/sql`, govulncheck |
|
||||
@@ -0,0 +1,132 @@
|
||||
---
|
||||
name: testing
|
||||
description: Best practices for software testing — TDD, test pyramid, F.I.R.S.T. principles, mocking strategies, and test organization. Use when writing tests, designing test strategy, refactoring under test, or whenever the user mentions "unit test," "integration test," "TDD," "test coverage," "mock," "stub," "e2e test," "Jest," "Vitest," "pytest," "cargo test," or "testing."
|
||||
---
|
||||
|
||||
# Testing Best Practices
|
||||
|
||||
## The Test Pyramid
|
||||
|
||||
```
|
||||
╱╲
|
||||
╱ E2E ╲ Few — critical user journeys
|
||||
╱────────╲
|
||||
╱ Integration ╲ Some — API, DB, external service boundaries
|
||||
╱────────────────╲
|
||||
╱ Unit Tests ╲ Many — domain logic, utilities, pure functions
|
||||
╱────────────────────╲
|
||||
```
|
||||
|
||||
- **Unit tests** — fast, isolated, test one behavior. 70%+ of tests.
|
||||
- **Integration tests** — test boundaries (DB queries, API contracts, file IO).
|
||||
- **E2E tests** — critical paths only. Slow and brittle — minimize.
|
||||
|
||||
## Three Laws of TDD
|
||||
|
||||
1. Don't write production code until you have a failing test.
|
||||
2. Don't write more of a test than is sufficient to fail.
|
||||
3. Don't write more production code than is sufficient to pass.
|
||||
|
||||
The cycle: Red (failing test) → Green (passing) → Refactor.
|
||||
|
||||
## F.I.R.S.T. Principles
|
||||
|
||||
- **Fast** — tests run quickly. If slow, they won't be run.
|
||||
- **Independent** — no test depends on another. Any order, any subset.
|
||||
- **Repeatable** — same result every time, in any environment.
|
||||
- **Self-validating** — pass/fail is binary. No manual inspection.
|
||||
- **Timely** — written *before* (or at the same time as) production code.
|
||||
|
||||
## Test Structure
|
||||
|
||||
### Arrange-Act-Assert (AAA)
|
||||
```typescript
|
||||
// Arrange
|
||||
const user = new User('test@example.com');
|
||||
const service = new AuthService(mockRepo);
|
||||
|
||||
// Act
|
||||
const result = await service.login(user);
|
||||
|
||||
// Assert
|
||||
expect(result.success).toBe(true);
|
||||
```
|
||||
|
||||
### Naming
|
||||
```typescript
|
||||
describe('CreateOrderUseCase', () => {
|
||||
it('throws when inventory is insufficient', async () => { ... });
|
||||
it('creates order with correct total', async () => { ... });
|
||||
it('deducts inventory on successful order', async () => { ... });
|
||||
});
|
||||
```
|
||||
|
||||
### One assertion per test? No — one *concept* per test.
|
||||
Group related assertions for the same behavior:
|
||||
```typescript
|
||||
it('returns complete user profile', () => {
|
||||
const profile = service.getProfile(userId);
|
||||
expect(profile.name).toBe('Alice');
|
||||
expect(profile.email).toBe('alice@example.com');
|
||||
expect(profile.role).toBe('admin');
|
||||
});
|
||||
```
|
||||
|
||||
## What to Test
|
||||
|
||||
| Test | What | Example |
|
||||
|------|------|---------|
|
||||
| Domain logic | Business rules, calculations, validations | `PriceCalculator.calculateTotal()` |
|
||||
| Edge cases | Empty state, null, max values, error paths | `Order.create({ items: [] })` |
|
||||
| Public contracts | API endpoints, method signatures | `POST /orders returns 201` |
|
||||
| Error handling | Expected failures, retry, fallback | `Repository.save() when DB down` |
|
||||
|
||||
## What NOT to Test
|
||||
|
||||
- ❌ Framework internals (React, Express, Drizzle — they have their own tests)
|
||||
- ❌ Implementation details (private methods — test through public API)
|
||||
- ❌ Simple one-liners with no logic (`getters`, `toString`)
|
||||
- ❌ Configuration constants
|
||||
|
||||
## Mocking Strategies
|
||||
|
||||
- **Mock external boundaries only** — database, network, filesystem, clock.
|
||||
- **Don't mock domain objects** — use real entities/value objects.
|
||||
- **Mock roles, not objects** — mock the interface/port, not the concrete class.
|
||||
- **Prefer fakes over mocks** for test doubles that have real behavior (e.g., in-memory DB).
|
||||
- **Over-mocking is a smell** — tests that break on every refactor are testing implementation, not behavior.
|
||||
|
||||
### Mock Levels
|
||||
```typescript
|
||||
// ❌ Over-mocked: tests break on refactor, test internal wiring
|
||||
const mockRepo = { save: vi.fn() };
|
||||
const useCase = new CreateOrder(mockRepo);
|
||||
mockRepo.save.mockResolvedValueOnce({ id: '1' });
|
||||
|
||||
// ✅ Better: test behavior through real fakes
|
||||
class InMemoryOrderRepo implements OrderRepository {
|
||||
private orders = new Map<string, Order>();
|
||||
async save(o: Order) { this.orders.set(o.id, o); return o; }
|
||||
async findById(id: string) { return this.orders.get(id) ?? null; }
|
||||
}
|
||||
```
|
||||
|
||||
## Test Coverage Guidelines
|
||||
|
||||
- **80-90% line coverage** is healthy for production code
|
||||
- **100% is a red flag** — likely testing trivia and implementation details
|
||||
- **Focus coverage on domain logic** (business rules) over infrastructure wrappers
|
||||
- **Coverage is a lagging indicator** — good tests aren't about coverage, they're about confidence
|
||||
|
||||
## Deeper Reference
|
||||
|
||||
When the task calls for it, load:
|
||||
|
||||
- **[references/mocks.md](references/mocks.md)** — Complete test double taxonomy (Dummy, Fake, Stub, Mock, Spy). Code examples, when to use each, over-mocking traps, and anti-patterns.
|
||||
|
||||
## Language-Specific Directives
|
||||
|
||||
- **TypeScript:** Use Vitest over Jest (faster, ESM-native). Use `vi.fn()` sparingly.
|
||||
- **Python:** Use pytest (not unittest). Fixtures over setup/teardown.
|
||||
- **Rust:** Unit tests inline in module. Integration tests in `tests/` directory.
|
||||
- **Go:** Tests in `_test.go` files. Table-driven tests for multiple cases.
|
||||
@@ -0,0 +1,135 @@
|
||||
# Test Doubles — Mock, Stub, Fake, Spy, Dummy
|
||||
|
||||
Understanding the differences prevents tests that are brittle, misleading, or hard to maintain.
|
||||
|
||||
## The Taxonomy (Meszaros, xUnit Test Patterns)
|
||||
|
||||
| Term | What It Is | When to Use |
|
||||
|------|-----------|-------------|
|
||||
| **Dummy** | Passed but never used. Fills parameter lists. | Satisfy constructor/parameter requirements that aren't exercised by this test. |
|
||||
| **Fake** | Working (but simplified) implementation. Uses real logic, just lighter. | In-memory DB, fake HTTP client, fake file system. **Preferred over mocks whenever possible.** |
|
||||
| **Stub** | Returns canned answers to calls made during the test. | When you need a consistent response (user exists, payment succeeded). |
|
||||
| **Mock** | Pre-programmed with expectations about *what calls will be made*. Verifies interactions. | When you need to verify that something was called correctly (e.g., notification was sent). |
|
||||
| **Spy** | Records calls for later verification. Wraps a real object. | When you want the real behavior but also need to verify calls. |
|
||||
|
||||
## The Continuum of Fidelity
|
||||
|
||||
```
|
||||
Minimal ──────────────────────────────────────────────→ Max fidelity
|
||||
Dummy → Stub → Spy → Mock → Fake (in-memory) → Real (integration)
|
||||
```
|
||||
|
||||
**Rule of thumb:** Use the **highest fidelity that's still fast and deterministic**. Prefer Fakes → Stubs → Mocks → Dummies. Default to fakes.
|
||||
|
||||
## Code Examples
|
||||
|
||||
### Dummy
|
||||
```typescript
|
||||
// Used only to satisfy type signature — never read in this test
|
||||
it('creates order with items', async () => {
|
||||
const dummyNotifier = { send: vi.fn() }; // never called in this path
|
||||
const order = new CreateOrderUseCase(new InMemoryOrderRepo(), dummyNotifier);
|
||||
// ... test only cares about order creation, not notification
|
||||
});
|
||||
```
|
||||
|
||||
### Fake
|
||||
```typescript
|
||||
// Has real behavior, just in-memory. No DB, no network.
|
||||
class FakeUserRepository implements UserRepository {
|
||||
private users = new Map<string, User>();
|
||||
|
||||
async findById(id: string) { return this.users.get(id) ?? null; }
|
||||
async save(user: User) { this.users.set(user.id, user); return user; }
|
||||
async exists(email: string) { return [...this.users.values()].some(u => u.email === email); }
|
||||
}
|
||||
|
||||
it('creates user', async () => {
|
||||
const repo = new FakeUserRepository();
|
||||
const svc = new UserService(repo);
|
||||
const user = await svc.create('a@b.com');
|
||||
expect(user.email).toBe('a@b.com');
|
||||
expect(await repo.exists('a@b.com')).toBe(true);
|
||||
});
|
||||
```
|
||||
|
||||
### Stub
|
||||
```typescript
|
||||
// Returns hardcoded answers — no real behavior, no verification
|
||||
const stubbedRepo = {
|
||||
findById: vi.fn().mockResolvedValue({ id: '1', name: 'Alice' }),
|
||||
save: vi.fn().mockResolvedValue({ id: '1', name: 'Alice' }),
|
||||
};
|
||||
|
||||
it('returns user when found', async () => {
|
||||
const svc = new UserService(stubbedRepo);
|
||||
const user = await svc.findById('1');
|
||||
expect(user?.name).toBe('Alice');
|
||||
});
|
||||
```
|
||||
|
||||
### Mock
|
||||
```typescript
|
||||
// Sets expectations about interactions. Use sparingly.
|
||||
it('sends notification on order', async () => {
|
||||
const notifyMock = vi.fn();
|
||||
const svc = new OrderService(new FakeOrderRepo(), notifyMock);
|
||||
await svc.create({ userId: '1', items: [...] });
|
||||
expect(notifyMock).toHaveBeenCalledWith('1', expect.stringContaining('order'));
|
||||
});
|
||||
```
|
||||
|
||||
### Spy
|
||||
```typescript
|
||||
// Wraps real behavior, records calls
|
||||
const repo = new FakeUserRepository();
|
||||
const spy = vi.spyOn(repo, 'save');
|
||||
const svc = new UserService(repo);
|
||||
await svc.create('a@b.com');
|
||||
expect(spy).toHaveBeenCalledOnce();
|
||||
```
|
||||
|
||||
## When to Mock vs Use Fakes
|
||||
|
||||
| Scenario | Use |
|
||||
|----------|-----|
|
||||
| The collaborator is deterministic (math, calculation) | Fake or real |
|
||||
| The collaborator touches external systems (DB, network, disk) | Fake (in-memory) or mock |
|
||||
| You need to verify something was called | Mock or spy |
|
||||
| You need a consistent response | Stub |
|
||||
| The collaborator doesn't matter for this test | Dummy or ignore |
|
||||
|
||||
## Mocking Best Practices
|
||||
|
||||
1. **Mock roles, not objects** — mock the interface/port, not the concrete class.
|
||||
2. **Don't mock domain objects** — use real entities/value objects. They have no IO, so there's no reason to mock them.
|
||||
3. **Over-mocking is a smell** — if tests break on every refactor, you're testing implementation, not behavior.
|
||||
4. **One mock per test, ideally** — many mocks means many expectations, means fragile tests.
|
||||
5. **Prefer `mockResolvedValue` (once) over `mockResolvedValue` (always)** — be explicit about test context.
|
||||
|
||||
### The Over-Mocking Trap
|
||||
|
||||
```typescript
|
||||
// ❌ Over-mocked — tests break when internals change
|
||||
it('creates order', async () => {
|
||||
const repo = { save: vi.fn() };
|
||||
const calc = { calculate: vi.fn().mockReturnValue(100) };
|
||||
const notify = { send: vi.fn() };
|
||||
// ... mocks everywhere, tests know the implementation
|
||||
|
||||
// ✅ Better — fakes for real behavior, mock only for verification
|
||||
it('creates order', async () => {
|
||||
const repo = new FakeOrderRepo();
|
||||
const calc = new PriceCalculator(); // real
|
||||
const notify = vi.fn(); // mock only what you need to verify
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
## Testing Anti-Patterns
|
||||
|
||||
- ❌ **Mocking everything** — tests that don't test the real behavior
|
||||
- ❌ **Mocking the SUT** — mocking the class you're testing
|
||||
- ❌ **Over-specification** — `expect(mock).toHaveBeenCalledTimes(1)` when "at least once" is fine
|
||||
- ❌ **Conditional mocks** — `mockReturnValueOnce` chains that break when order changes
|
||||
- ❌ **Partial mocks** — mocking some methods but not others on the real object (spy is better)
|
||||
@@ -0,0 +1,176 @@
|
||||
---
|
||||
name: typescript
|
||||
description: TypeScript best practices — strict mode, type patterns, generics, module system, async patterns, and project organization. Use when writing TypeScript code, configuring tsconfig, or whenever the user mentions "TypeScript," "TS," "ESM," "deno," "bun," "type annotation," "generics," "interface," "type," "strict mode," or "tsconfig."
|
||||
---
|
||||
|
||||
# TypeScript Best Practices
|
||||
|
||||
## Configuration
|
||||
|
||||
```jsonc
|
||||
// tsconfig.json — strict mode is non-negotiable
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true, // Enable all strict checks
|
||||
"noUncheckedIndexedAccess": true, // Accessing arrays/objects is safe
|
||||
"exactOptionalPropertyTypes": true,
|
||||
"noImplicitReturns": true,
|
||||
"esModuleInterop": true,
|
||||
"moduleResolution": "bundler", // or "node16" for ESM
|
||||
"target": "ESNext",
|
||||
"isolatedModules": true,
|
||||
"skipLibCheck": true // Fast, skip node_modules type check
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Types vs Interfaces
|
||||
|
||||
```typescript
|
||||
// Prefer `interface` for public API contracts (extends, implements)
|
||||
interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
// Prefer `type` for unions, intersections, complex types
|
||||
type Result<T> = { ok: true; value: T } | { ok: false; error: Error };
|
||||
type Status = 'active' | 'inactive' | 'suspended';
|
||||
type CreateUserInput = { name: string } & BaseInput;
|
||||
```
|
||||
|
||||
**Rule of thumb:** `interface` for objects, `type` for everything else.
|
||||
|
||||
## Generics — Use Intentionally
|
||||
|
||||
```typescript
|
||||
// ✅ Good — specific enough
|
||||
function mapValues<K extends string, V, R>(
|
||||
obj: Record<K, V>,
|
||||
fn: (value: V, key: K) => R
|
||||
): Record<K, R> { ... }
|
||||
|
||||
// ❌ Bad — over-generic, loses type info
|
||||
function identity(value: any): any { ... }
|
||||
|
||||
// ✅ Good — preserves type
|
||||
function identity<T>(value: T): T { return value; }
|
||||
```
|
||||
|
||||
## Async Patterns
|
||||
|
||||
- **Top-level await** — OK in ESM modules, but prefer `main()` pattern.
|
||||
- **Promise.all for parallel** — don't `await` in sequence if requests are independent.
|
||||
- **Errors** — always handle Promise rejections. No unhandled promises.
|
||||
- **Async iterators** — `for await (const item of stream)` for paginated data.
|
||||
|
||||
```typescript
|
||||
// ❌ Sequential
|
||||
const a = await fetchA();
|
||||
const b = await fetchB(); // waits for A
|
||||
|
||||
// ✅ Parallel
|
||||
const [a, b] = await Promise.all([fetchA(), fetchB()]);
|
||||
```
|
||||
|
||||
## No `any`
|
||||
|
||||
```typescript
|
||||
// ❌ Never — disables all type checks
|
||||
function parse(input: any): any { ... }
|
||||
|
||||
// ✅ Use `unknown` instead — forces type narrowing before use
|
||||
function parse(input: unknown): Result<Data, ParseError> { ... }
|
||||
|
||||
// ✅ Use `never` for exhaustive checks
|
||||
function assertNever(x: never): never { throw new Error('Unexpected: ' + x); }
|
||||
```
|
||||
|
||||
## Module System
|
||||
|
||||
- **ESM only.** No `require()`. Use `import` / `export`.
|
||||
- **Named exports** over default exports (better tree-shaking, rename safety).
|
||||
- **Barrel exports** (`index.ts`) — use sparingly. Can cause circular deps and slow builds.
|
||||
- **Path aliases** — `@/` or `~/` for internal imports. Configure in tsconfig.
|
||||
|
||||
```typescript
|
||||
// Always use .js/.mjs extension for relative imports in ESM
|
||||
import { User } from './user.js';
|
||||
import { createOrder } from '@/use-cases/create-order.js';
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── domain/ # Pure business logic, entities, value objects
|
||||
│ ├── user.ts
|
||||
│ └── order.ts
|
||||
├── application/ # Use cases, ports
|
||||
│ ├── create-order.ts
|
||||
│ └── user-repository.ts # port (interface)
|
||||
├── infrastructure/ # Adapters (DB, HTTP, queue)
|
||||
│ └── postgres-user-repo.ts
|
||||
├── api/ # HTTP handlers, middleware
|
||||
│ ├── routes/
|
||||
│ └── middleware/
|
||||
├── lib/ # Shared utilities (pure, no framework deps)
|
||||
└── index.ts # Composition root, entry point
|
||||
```
|
||||
|
||||
## Enum vs Union
|
||||
|
||||
```typescript
|
||||
// ❌ Enums — runtime overhead, not tree-shakeable, const enum has issues
|
||||
enum Status { Active, Inactive }
|
||||
|
||||
// ✅ Union types — zero-cost, works everywhere
|
||||
type Status = 'active' | 'inactive';
|
||||
|
||||
// ✅ Const objects with `as const` — when you need both type and runtime
|
||||
const STATUS = { ACTIVE: 'active', INACTIVE: 'inactive' } as const;
|
||||
type Status = (typeof STATUS)[keyof typeof STATUS];
|
||||
```
|
||||
|
||||
## Branded Types for IDs
|
||||
|
||||
```typescript
|
||||
// Type-safe IDs — prevents mixing up different entity IDs
|
||||
type Brand<T, B> = T & { __brand: B };
|
||||
type UserId = Brand<string, 'UserId'>;
|
||||
type OrderId = Brand<string, 'OrderId'>;
|
||||
|
||||
function getUser(id: UserId) { ... }
|
||||
function getOrder(id: OrderId) { ... }
|
||||
getUser('abc' as UserId); // OK
|
||||
getUser(orderId); // ❌ Type error — OrderId not assignable to UserId
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
```typescript
|
||||
// Domain errors as discriminated unions
|
||||
type CreateUserError = ValidationError | DuplicateEmailError | DatabaseError;
|
||||
type ValidationError = { kind: 'validation'; field: string; message: string };
|
||||
type DuplicateEmailError = { kind: 'duplicate_email'; email: string };
|
||||
|
||||
function createUser(input: CreateUserInput): Result<User, CreateUserError> { ... }
|
||||
|
||||
// Use branded Result in use cases, throw for unexpected infrastructure errors
|
||||
class DatabaseError extends Error { constructor(cause: unknown) { super(); this.cause = cause; } }
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
- **Vitest** over Jest (faster, ESM-native, TypeScript-native).
|
||||
- **`vi.fn()`** for mocks. Prefer fakes (in-memory implementations).
|
||||
- **Cover 'as const', `satisfies`, `z.infer` patterns** — they are compile-time only.
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- ❌ `any` — disables the type system entirely
|
||||
- ❌ `as` type assertions (`value as Type`) — use proper narrowing or Zod parsing
|
||||
- ❌ `!` non-null assertion (`user!.name`) — defeats strict null checks
|
||||
- ❌ Namespace — use ES modules instead
|
||||
- ❌ `Function` type — use typed function signature `(args: Args) => Result`
|
||||
- ❌ Optional chaining chains — `a?.b?.c?.d` is fragile. Narrow earlier.
|
||||
Reference in New Issue
Block a user