feat(bundles): add editorial bundle plugins

This commit is contained in:
sickn33
2026-03-27 08:48:03 +01:00
parent 8eff08b706
commit dffac91d3b
1052 changed files with 212282 additions and 68 deletions

View File

@@ -0,0 +1,33 @@
{
"name": "antigravity-bundle-full-stack-developer",
"version": "8.10.0",
"description": "Install the \"Full-Stack Developer\" editorial skill bundle from Antigravity Awesome Skills.",
"author": {
"name": "sickn33 and contributors",
"url": "https://github.com/sickn33/antigravity-awesome-skills"
},
"homepage": "https://github.com/sickn33/antigravity-awesome-skills",
"repository": "https://github.com/sickn33/antigravity-awesome-skills",
"license": "MIT",
"keywords": [
"codex",
"skills",
"bundle",
"full-stack-developer",
"productivity"
],
"skills": "./skills/",
"interface": {
"displayName": "Full-Stack Developer",
"shortDescription": "Web Development · 6 curated skills",
"longDescription": "For end-to-end web application development. Covers Senior Fullstack, Frontend Developer, and 4 more skills.",
"developerName": "sickn33 and contributors",
"category": "Web Development",
"capabilities": [
"Interactive",
"Write"
],
"websiteURL": "https://github.com/sickn33/antigravity-awesome-skills",
"brandColor": "#111827"
}
}

View File

@@ -0,0 +1,85 @@
---
name: api-patterns
description: "API design principles and decision-making. REST vs GraphQL vs tRPC selection, response formats, versioning, pagination."
risk: unknown
source: community
date_added: "2026-02-27"
---
# API Patterns
> API design principles and decision-making for 2025.
> **Learn to THINK, not copy fixed patterns.**
## 🎯 Selective Reading Rule
**Read ONLY files relevant to the request!** Check the content map, find what you need.
---
## 📑 Content Map
| File | Description | When to Read |
|------|-------------|--------------|
| `api-style.md` | REST vs GraphQL vs tRPC decision tree | Choosing API type |
| `rest.md` | Resource naming, HTTP methods, status codes | Designing REST API |
| `response.md` | Envelope pattern, error format, pagination | Response structure |
| `graphql.md` | Schema design, when to use, security | Considering GraphQL |
| `trpc.md` | TypeScript monorepo, type safety | TS fullstack projects |
| `versioning.md` | URI/Header/Query versioning | API evolution planning |
| `auth.md` | JWT, OAuth, Passkey, API Keys | Auth pattern selection |
| `rate-limiting.md` | Token bucket, sliding window | API protection |
| `documentation.md` | OpenAPI/Swagger best practices | Documentation |
| `security-testing.md` | OWASP API Top 10, auth/authz testing | Security audits |
---
## 🔗 Related Skills
| Need | Skill |
|------|-------|
| API implementation | `@[skills/backend-development]` |
| Data structure | `@[skills/database-design]` |
| Security details | `@[skills/security-hardening]` |
---
## ✅ Decision Checklist
Before designing an API:
- [ ] **Asked user about API consumers?**
- [ ] **Chosen API style for THIS context?** (REST/GraphQL/tRPC)
- [ ] **Defined consistent response format?**
- [ ] **Planned versioning strategy?**
- [ ] **Considered authentication needs?**
- [ ] **Planned rate limiting?**
- [ ] **Documentation approach defined?**
---
## ❌ Anti-Patterns
**DON'T:**
- Default to REST for everything
- Use verbs in REST endpoints (/getUsers)
- Return inconsistent response formats
- Expose internal errors to clients
- Skip rate limiting
**DO:**
- Choose API style based on context
- Ask about client requirements
- Document thoroughly
- Use appropriate status codes
---
## Script
| Script | Purpose | Command |
|--------|---------|---------|
| `scripts/api_validator.py` | API endpoint validation | `python scripts/api_validator.py <project_path>` |
## When to Use
This skill is applicable to execute the workflow or actions described in the overview.

View File

@@ -0,0 +1,42 @@
# API Style Selection (2025)
> REST vs GraphQL vs tRPC - Hangi durumda hangisi?
## Decision Tree
```
Who are the API consumers?
├── Public API / Multiple platforms
│ └── REST + OpenAPI (widest compatibility)
├── Complex data needs / Multiple frontends
│ └── GraphQL (flexible queries)
├── TypeScript frontend + backend (monorepo)
│ └── tRPC (end-to-end type safety)
├── Real-time / Event-driven
│ └── WebSocket + AsyncAPI
└── Internal microservices
└── gRPC (performance) or REST (simplicity)
```
## Comparison
| Factor | REST | GraphQL | tRPC |
|--------|------|---------|------|
| **Best for** | Public APIs | Complex apps | TS monorepos |
| **Learning curve** | Low | Medium | Low (if TS) |
| **Over/under fetching** | Common | Solved | Solved |
| **Type safety** | Manual (OpenAPI) | Schema-based | Automatic |
| **Caching** | HTTP native | Complex | Client-based |
## Selection Questions
1. Who are the API consumers?
2. Is the frontend TypeScript?
3. How complex are the data relationships?
4. Is caching critical?
5. Public or internal API?

View File

@@ -0,0 +1,24 @@
# Authentication Patterns
> Choose auth pattern based on use case.
## Selection Guide
| Pattern | Best For |
|---------|----------|
| **JWT** | Stateless, microservices |
| **Session** | Traditional web, simple |
| **OAuth 2.0** | Third-party integration |
| **API Keys** | Server-to-server, public APIs |
| **Passkey** | Modern passwordless (2025+) |
## JWT Principles
```
Important:
├── Always verify signature
├── Check expiration
├── Include minimal claims
├── Use short expiry + refresh tokens
└── Never store sensitive data in JWT
```

View File

@@ -0,0 +1,26 @@
# API Documentation Principles
> Good docs = happy developers = API adoption.
## OpenAPI/Swagger Essentials
```
Include:
├── All endpoints with examples
├── Request/response schemas
├── Authentication requirements
├── Error response formats
└── Rate limiting info
```
## Good Documentation Has
```
Essentials:
├── Quick start / Getting started
├── Authentication guide
├── Complete API reference
├── Error handling guide
├── Code examples (multiple languages)
└── Changelog
```

View File

@@ -0,0 +1,41 @@
# GraphQL Principles
> Flexible queries for complex, interconnected data.
## When to Use
```
✅ Good fit:
├── Complex, interconnected data
├── Multiple frontend platforms
├── Clients need flexible queries
├── Evolving data requirements
└── Reducing over-fetching matters
❌ Poor fit:
├── Simple CRUD operations
├── File upload heavy
├── HTTP caching important
└── Team unfamiliar with GraphQL
```
## Schema Design Principles
```
Principles:
├── Think in graphs, not endpoints
├── Design for evolvability (no versions)
├── Use connections for pagination
├── Be specific with types (not generic "data")
└── Handle nullability thoughtfully
```
## Security Considerations
```
Protect against:
├── Query depth attacks → Set max depth
├── Query complexity → Calculate cost
├── Batching abuse → Limit batch size
├── Introspection → Disable in production
```

View File

@@ -0,0 +1,31 @@
# Rate Limiting Principles
> Protect your API from abuse and overload.
## Why Rate Limit
```
Protect against:
├── Brute force attacks
├── Resource exhaustion
├── Cost overruns (if pay-per-use)
└── Unfair usage
```
## Strategy Selection
| Type | How | When |
|------|-----|------|
| **Token bucket** | Burst allowed, refills over time | Most APIs |
| **Sliding window** | Smooth distribution | Strict limits |
| **Fixed window** | Simple counters per window | Basic needs |
## Response Headers
```
Include in headers:
├── X-RateLimit-Limit (max requests)
├── X-RateLimit-Remaining (requests left)
├── X-RateLimit-Reset (when limit resets)
└── Return 429 when exceeded
```

View File

@@ -0,0 +1,37 @@
# Response Format Principles
> Consistency is key - choose a format and stick to it.
## Common Patterns
```
Choose one:
├── Envelope pattern ({ success, data, error })
├── Direct data (just return the resource)
└── HAL/JSON:API (hypermedia)
```
## Error Response
```
Include:
├── Error code (for programmatic handling)
├── User message (for display)
├── Details (for debugging, field-level errors)
├── Request ID (for support)
└── NOT internal details (security!)
```
## Pagination Types
| Type | Best For | Trade-offs |
|------|----------|------------|
| **Offset** | Simple, jumpable | Performance on large datasets |
| **Cursor** | Large datasets | Can't jump to page |
| **Keyset** | Performance critical | Requires sortable key |
### Selection Questions
1. How large is the dataset?
2. Do users need to jump to specific pages?
3. Is data frequently changing?

View File

@@ -0,0 +1,40 @@
# REST Principles
> Resource-based API design - nouns not verbs.
## Resource Naming Rules
```
Principles:
├── Use NOUNS, not verbs (resources, not actions)
├── Use PLURAL forms (/users not /user)
├── Use lowercase with hyphens (/user-profiles)
├── Nest for relationships (/users/123/posts)
└── Keep shallow (max 3 levels deep)
```
## HTTP Method Selection
| Method | Purpose | Idempotent? | Body? |
|--------|---------|-------------|-------|
| **GET** | Read resource(s) | Yes | No |
| **POST** | Create new resource | No | Yes |
| **PUT** | Replace entire resource | Yes | Yes |
| **PATCH** | Partial update | No | Yes |
| **DELETE** | Remove resource | Yes | No |
## Status Code Selection
| Situation | Code | Why |
|-----------|------|-----|
| Success (read) | 200 | Standard success |
| Created | 201 | New resource created |
| No content | 204 | Success, nothing to return |
| Bad request | 400 | Malformed request |
| Unauthorized | 401 | Missing/invalid auth |
| Forbidden | 403 | Valid auth, no permission |
| Not found | 404 | Resource doesn't exist |
| Conflict | 409 | State conflict (duplicate) |
| Validation error | 422 | Valid syntax, invalid data |
| Rate limited | 429 | Too many requests |
| Server error | 500 | Our fault |

View File

@@ -0,0 +1,211 @@
#!/usr/bin/env python3
"""
API Validator - Checks API endpoints for best practices.
Validates OpenAPI specs, response formats, and common issues.
"""
import sys
import json
import re
from pathlib import Path
# Fix Windows console encoding for Unicode output
try:
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
sys.stderr.reconfigure(encoding='utf-8', errors='replace')
except AttributeError:
pass # Python < 3.7
def find_api_files(project_path: Path) -> list:
"""Find API-related files."""
patterns = [
"**/*api*.ts", "**/*api*.js", "**/*api*.py",
"**/routes/*.ts", "**/routes/*.js", "**/routes/*.py",
"**/controllers/*.ts", "**/controllers/*.js",
"**/endpoints/*.ts", "**/endpoints/*.py",
"**/*.openapi.json", "**/*.openapi.yaml",
"**/swagger.json", "**/swagger.yaml",
"**/openapi.json", "**/openapi.yaml"
]
files = []
for pattern in patterns:
files.extend(project_path.glob(pattern))
# Exclude node_modules, etc.
return [f for f in files if not any(x in str(f) for x in ['node_modules', '.git', 'dist', 'build', '__pycache__'])]
def check_openapi_spec(file_path: Path) -> dict:
"""Check OpenAPI/Swagger specification."""
issues = []
passed = []
try:
content = file_path.read_text(encoding='utf-8')
if file_path.suffix == '.json':
spec = json.loads(content)
else:
# Basic YAML check
if 'openapi:' in content or 'swagger:' in content:
passed.append("[OK] OpenAPI/Swagger version defined")
else:
issues.append("[X] No OpenAPI version found")
if 'paths:' in content:
passed.append("[OK] Paths section exists")
else:
issues.append("[X] No paths defined")
if 'components:' in content or 'definitions:' in content:
passed.append("[OK] Schema components defined")
return {'file': str(file_path), 'passed': passed, 'issues': issues, 'type': 'openapi'}
# JSON OpenAPI checks
if 'openapi' in spec or 'swagger' in spec:
passed.append("[OK] OpenAPI version defined")
if 'info' in spec:
if 'title' in spec['info']:
passed.append("[OK] API title defined")
if 'version' in spec['info']:
passed.append("[OK] API version defined")
if 'description' not in spec['info']:
issues.append("[!] API description missing")
if 'paths' in spec:
path_count = len(spec['paths'])
passed.append(f"[OK] {path_count} endpoints defined")
# Check each path
for path, methods in spec['paths'].items():
for method, details in methods.items():
if method in ['get', 'post', 'put', 'patch', 'delete']:
if 'responses' not in details:
issues.append(f"[X] {method.upper()} {path}: No responses defined")
if 'summary' not in details and 'description' not in details:
issues.append(f"[!] {method.upper()} {path}: No description")
except Exception as e:
issues.append(f"[X] Parse error: {e}")
return {'file': str(file_path), 'passed': passed, 'issues': issues, 'type': 'openapi'}
def check_api_code(file_path: Path) -> dict:
"""Check API code for common issues."""
issues = []
passed = []
try:
content = file_path.read_text(encoding='utf-8')
# Check for error handling
error_patterns = [
r'try\s*{', r'try:', r'\.catch\(',
r'except\s+', r'catch\s*\('
]
has_error_handling = any(re.search(p, content) for p in error_patterns)
if has_error_handling:
passed.append("[OK] Error handling present")
else:
issues.append("[X] No error handling found")
# Check for status codes
status_patterns = [
r'status\s*\(\s*\d{3}\s*\)', r'statusCode\s*[=:]\s*\d{3}',
r'HttpStatus\.', r'status_code\s*=\s*\d{3}',
r'\.status\(\d{3}\)', r'res\.status\('
]
has_status = any(re.search(p, content) for p in status_patterns)
if has_status:
passed.append("[OK] HTTP status codes used")
else:
issues.append("[!] No explicit HTTP status codes")
# Check for validation
validation_patterns = [
r'validate', r'schema', r'zod', r'joi', r'yup',
r'pydantic', r'@Body\(', r'@Query\('
]
has_validation = any(re.search(p, content, re.I) for p in validation_patterns)
if has_validation:
passed.append("[OK] Input validation present")
else:
issues.append("[!] No input validation detected")
# Check for auth middleware
auth_patterns = [
r'auth', r'jwt', r'bearer', r'token',
r'middleware', r'guard', r'@Authenticated'
]
has_auth = any(re.search(p, content, re.I) for p in auth_patterns)
if has_auth:
passed.append("[OK] Authentication/authorization detected")
# Check for rate limiting
rate_patterns = [r'rateLimit', r'throttle', r'rate.?limit']
has_rate = any(re.search(p, content, re.I) for p in rate_patterns)
if has_rate:
passed.append("[OK] Rate limiting present")
# Check for logging
log_patterns = [r'console\.log', r'logger\.', r'logging\.', r'log\.']
has_logging = any(re.search(p, content) for p in log_patterns)
if has_logging:
passed.append("[OK] Logging present")
except Exception as e:
issues.append(f"[X] Read error: {e}")
return {'file': str(file_path), 'passed': passed, 'issues': issues, 'type': 'code'}
def main():
target = sys.argv[1] if len(sys.argv) > 1 else "."
project_path = Path(target)
print("\n" + "=" * 60)
print(" API VALIDATOR - Endpoint Best Practices Check")
print("=" * 60 + "\n")
api_files = find_api_files(project_path)
if not api_files:
print("[!] No API files found.")
print(" Looking for: routes/, controllers/, api/, openapi.json/yaml")
sys.exit(0)
results = []
for file_path in api_files[:15]: # Limit
if 'openapi' in file_path.name.lower() or 'swagger' in file_path.name.lower():
result = check_openapi_spec(file_path)
else:
result = check_api_code(file_path)
results.append(result)
# Print results
total_issues = 0
total_passed = 0
for result in results:
print(f"\n[FILE] {result['file']} [{result['type']}]")
for item in result['passed']:
print(f" {item}")
total_passed += 1
for item in result['issues']:
print(f" {item}")
if item.startswith("[X]"):
total_issues += 1
print("\n" + "=" * 60)
print(f"[RESULTS] {total_passed} passed, {total_issues} critical issues")
print("=" * 60)
if total_issues == 0:
print("[OK] API validation passed")
sys.exit(0)
else:
print("[X] Fix critical issues before deployment")
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,122 @@
# API Security Testing
> Principles for testing API security. OWASP API Top 10, authentication, authorization testing.
---
## OWASP API Security Top 10
| Vulnerability | Test Focus |
|---------------|------------|
| **API1: BOLA** | Access other users' resources |
| **API2: Broken Auth** | JWT, session, credentials |
| **API3: Property Auth** | Mass assignment, data exposure |
| **API4: Resource Consumption** | Rate limiting, DoS |
| **API5: Function Auth** | Admin endpoints, role bypass |
| **API6: Business Flow** | Logic abuse, automation |
| **API7: SSRF** | Internal network access |
| **API8: Misconfiguration** | Debug endpoints, CORS |
| **API9: Inventory** | Shadow APIs, old versions |
| **API10: Unsafe Consumption** | Third-party API trust |
---
## Authentication Testing
### JWT Testing
| Check | What to Test |
|-------|--------------|
| Algorithm | None, algorithm confusion |
| Secret | Weak secrets, brute force |
| Claims | Expiration, issuer, audience |
| Signature | Manipulation, key injection |
### Session Testing
| Check | What to Test |
|-------|--------------|
| Generation | Predictability |
| Storage | Client-side security |
| Expiration | Timeout enforcement |
| Invalidation | Logout effectiveness |
---
## Authorization Testing
| Test Type | Approach |
|-----------|----------|
| **Horizontal** | Access peer users' data |
| **Vertical** | Access higher privilege functions |
| **Context** | Access outside allowed scope |
### BOLA/IDOR Testing
1. Identify resource IDs in requests
2. Capture request with user A's session
3. Replay with user B's session
4. Check for unauthorized access
---
## Input Validation Testing
| Injection Type | Test Focus |
|----------------|------------|
| SQL | Query manipulation |
| NoSQL | Document queries |
| Command | System commands |
| LDAP | Directory queries |
**Approach:** Test all parameters, try type coercion, test boundaries, check error messages.
---
## Rate Limiting Testing
| Aspect | Check |
|--------|-------|
| Existence | Is there any limit? |
| Bypass | Headers, IP rotation |
| Scope | Per-user, per-IP, global |
**Bypass techniques:** X-Forwarded-For, different HTTP methods, case variations, API versioning.
---
## GraphQL Security
| Test | Focus |
|------|-------|
| Introspection | Schema disclosure |
| Batching | Query DoS |
| Nesting | Depth-based DoS |
| Authorization | Field-level access |
---
## Security Testing Checklist
**Authentication:**
- [ ] Test for bypass
- [ ] Check credential strength
- [ ] Verify token security
**Authorization:**
- [ ] Test BOLA/IDOR
- [ ] Check privilege escalation
- [ ] Verify function access
**Input:**
- [ ] Test all parameters
- [ ] Check for injection
**Config:**
- [ ] Check CORS
- [ ] Verify headers
- [ ] Test error handling
---
> **Remember:** APIs are the backbone of modern apps. Test them like attackers will.

View File

@@ -0,0 +1,41 @@
# tRPC Principles
> End-to-end type safety for TypeScript monorepos.
## When to Use
```
✅ Perfect fit:
├── TypeScript on both ends
├── Monorepo structure
├── Internal tools
├── Rapid development
└── Type safety critical
❌ Poor fit:
├── Non-TypeScript clients
├── Public API
├── Need REST conventions
└── Multiple language backends
```
## Key Benefits
```
Why tRPC:
├── Zero schema maintenance
├── End-to-end type inference
├── IDE autocomplete across stack
├── Instant API changes reflected
└── No code generation step
```
## Integration Patterns
```
Common setups:
├── Next.js + tRPC (most common)
├── Monorepo with shared types
├── Remix + tRPC
└── Any TS frontend + backend
```

View File

@@ -0,0 +1,22 @@
# Versioning Strategies
> Plan for API evolution from day one.
## Decision Factors
| Strategy | Implementation | Trade-offs |
|----------|---------------|------------|
| **URI** | /v1/users | Clear, easy caching |
| **Header** | Accept-Version: 1 | Cleaner URLs, harder discovery |
| **Query** | ?version=1 | Easy to add, messy |
| **None** | Evolve carefully | Best for internal, risky for public |
## Versioning Philosophy
```
Consider:
├── Public API? → Version in URI
├── Internal only? → May not need versioning
├── GraphQL? → Typically no versions (evolve schema)
├── tRPC? → Types enforce compatibility
```

View File

@@ -0,0 +1,347 @@
---
name: backend-dev-guidelines
description: "You are a senior backend engineer operating production-grade services under strict architectural and reliability constraints. Use when routes, controllers, services, repositories, express middleware, or prisma database access."
risk: unknown
source: community
date_added: "2026-02-27"
---
# Backend Development Guidelines
**(Node.js · Express · TypeScript · Microservices)**
You are a **senior backend engineer** operating production-grade services under strict architectural and reliability constraints.
Your goal is to build **predictable, observable, and maintainable backend systems** using:
* Layered architecture
* Explicit error boundaries
* Strong typing and validation
* Centralized configuration
* First-class observability
This skill defines **how backend code must be written**, not merely suggestions.
---
## 1. Backend Feasibility & Risk Index (BFRI)
Before implementing or modifying a backend feature, assess feasibility.
### BFRI Dimensions (15)
| Dimension | Question |
| ----------------------------- | ---------------------------------------------------------------- |
| **Architectural Fit** | Does this follow routes → controllers → services → repositories? |
| **Business Logic Complexity** | How complex is the domain logic? |
| **Data Risk** | Does this affect critical data paths or transactions? |
| **Operational Risk** | Does this impact auth, billing, messaging, or infra? |
| **Testability** | Can this be reliably unit + integration tested? |
### Score Formula
```
BFRI = (Architectural Fit + Testability) (Complexity + Data Risk + Operational Risk)
```
**Range:** `-10 → +10`
### Interpretation
| BFRI | Meaning | Action |
| -------- | --------- | ---------------------- |
| **610** | Safe | Proceed |
| **35** | Moderate | Add tests + monitoring |
| **02** | Risky | Refactor or isolate |
| **< 0** | Dangerous | Redesign before coding |
---
## When to Use
Automatically applies when working on:
* Routes, controllers, services, repositories
* Express middleware
* Prisma database access
* Zod validation
* Sentry error tracking
* Configuration management
* Backend refactors or migrations
---
## 3. Core Architecture Doctrine (Non-Negotiable)
### 1. Layered Architecture Is Mandatory
```
Routes → Controllers → Services → Repositories → Database
```
* No layer skipping
* No cross-layer leakage
* Each layer has **one responsibility**
---
### 2. Routes Only Route
```ts
// ❌ NEVER
router.post('/create', async (req, res) => {
await prisma.user.create(...);
});
// ✅ ALWAYS
router.post('/create', (req, res) =>
userController.create(req, res)
);
```
Routes must contain **zero business logic**.
---
### 3. Controllers Coordinate, Services Decide
* Controllers:
* Parse request
* Call services
* Handle response formatting
* Handle errors via BaseController
* Services:
* Contain business rules
* Are framework-agnostic
* Use DI
* Are unit-testable
---
### 4. All Controllers Extend `BaseController`
```ts
export class UserController extends BaseController {
async getUser(req: Request, res: Response): Promise<void> {
try {
const user = await this.userService.getById(req.params.id);
this.handleSuccess(res, user);
} catch (error) {
this.handleError(error, res, 'getUser');
}
}
}
```
No raw `res.json` calls outside BaseController helpers.
---
### 5. All Errors Go to Sentry
```ts
catch (error) {
Sentry.captureException(error);
throw error;
}
```
`console.log`
❌ silent failures
❌ swallowed errors
---
### 6. unifiedConfig Is the Only Config Source
```ts
// ❌ NEVER
process.env.JWT_SECRET;
// ✅ ALWAYS
import { config } from '@/config/unifiedConfig';
config.auth.jwtSecret;
```
---
### 7. Validate All External Input with Zod
* Request bodies
* Query params
* Route params
* Webhook payloads
```ts
const schema = z.object({
email: z.string().email(),
});
const input = schema.parse(req.body);
```
No validation = bug.
---
## 4. Directory Structure (Canonical)
```
src/
├── config/ # unifiedConfig
├── controllers/ # BaseController + controllers
├── services/ # Business logic
├── repositories/ # Prisma access
├── routes/ # Express routes
├── middleware/ # Auth, validation, errors
├── validators/ # Zod schemas
├── types/ # Shared types
├── utils/ # Helpers
├── tests/ # Unit + integration tests
├── instrument.ts # Sentry (FIRST IMPORT)
├── app.ts # Express app
└── server.ts # HTTP server
```
---
## 5. Naming Conventions (Strict)
| Layer | Convention |
| ---------- | ------------------------- |
| Controller | `PascalCaseController.ts` |
| Service | `camelCaseService.ts` |
| Repository | `PascalCaseRepository.ts` |
| Routes | `camelCaseRoutes.ts` |
| Validators | `camelCase.schema.ts` |
---
## 6. Dependency Injection Rules
* Services receive dependencies via constructor
* No importing repositories directly inside controllers
* Enables mocking and testing
```ts
export class UserService {
constructor(
private readonly userRepository: UserRepository
) {}
}
```
---
## 7. Prisma & Repository Rules
* Prisma client **never used directly in controllers**
* Repositories:
* Encapsulate queries
* Handle transactions
* Expose intent-based methods
```ts
await userRepository.findActiveUsers();
```
---
## 8. Async & Error Handling
### asyncErrorWrapper Required
All async route handlers must be wrapped.
```ts
router.get(
'/users',
asyncErrorWrapper((req, res) =>
controller.list(req, res)
)
);
```
No unhandled promise rejections.
---
## 9. Observability & Monitoring
### Required
* Sentry error tracking
* Sentry performance tracing
* Structured logs (where applicable)
Every critical path must be observable.
---
## 10. Testing Discipline
### Required Tests
* **Unit tests** for services
* **Integration tests** for routes
* **Repository tests** for complex queries
```ts
describe('UserService', () => {
it('creates a user', async () => {
expect(user).toBeDefined();
});
});
```
No tests → no merge.
---
## 11. Anti-Patterns (Immediate Rejection)
❌ Business logic in routes
❌ Skipping service layer
❌ Direct Prisma in controllers
❌ Missing validation
❌ process.env usage
❌ console.log instead of Sentry
❌ Untested business logic
---
## 12. Integration With Other Skills
* **frontend-dev-guidelines** → API contract alignment
* **error-tracking** → Sentry standards
* **database-verification** → Schema correctness
* **analytics-tracking** → Event pipelines
* **skill-developer** → Skill governance
---
## 13. Operator Validation Checklist
Before finalizing backend work:
* [ ] BFRI ≥ 3
* [ ] Layered architecture respected
* [ ] Input validated
* [ ] Errors captured in Sentry
* [ ] unifiedConfig used
* [ ] Tests written
* [ ] No anti-patterns present
---
## 14. Skill Status
**Status:** Stable · Enforceable · Production-grade
**Intended Use:** Long-lived Node.js microservices with real traffic and real risk
---
## When to Use
This skill is applicable to execute the workflow or actions described in the overview.

View File

@@ -0,0 +1,451 @@
# Architecture Overview - Backend Services
Complete guide to the layered architecture pattern used in backend microservices.
## Table of Contents
- [Layered Architecture Pattern](#layered-architecture-pattern)
- [Request Lifecycle](#request-lifecycle)
- [Service Comparison](#service-comparison)
- [Directory Structure Rationale](#directory-structure-rationale)
- [Module Organization](#module-organization)
- [Separation of Concerns](#separation-of-concerns)
---
## Layered Architecture Pattern
### The Four Layers
```
┌─────────────────────────────────────┐
│ HTTP Request │
└───────────────┬─────────────────────┘
┌─────────────────────────────────────┐
│ Layer 1: ROUTES │
│ - Route definitions only │
│ - Middleware registration │
│ - Delegate to controllers │
│ - NO business logic │
└───────────────┬─────────────────────┘
┌─────────────────────────────────────┐
│ Layer 2: CONTROLLERS │
│ - Request/response handling │
│ - Input validation │
│ - Call services │
│ - Format responses │
│ - Error handling │
└───────────────┬─────────────────────┘
┌─────────────────────────────────────┐
│ Layer 3: SERVICES │
│ - Business logic │
│ - Orchestration │
│ - Call repositories │
│ - No HTTP knowledge │
└───────────────┬─────────────────────┘
┌─────────────────────────────────────┐
│ Layer 4: REPOSITORIES │
│ - Data access abstraction │
│ - Prisma operations │
│ - Query optimization │
│ - Caching │
└───────────────┬─────────────────────┘
┌─────────────────────────────────────┐
│ Database (MySQL) │
└─────────────────────────────────────┘
```
### Why This Architecture?
**Testability:**
- Each layer can be tested independently
- Easy to mock dependencies
- Clear test boundaries
**Maintainability:**
- Changes isolated to specific layers
- Business logic separate from HTTP concerns
- Easy to locate bugs
**Reusability:**
- Services can be used by routes, cron jobs, scripts
- Repositories hide database implementation
- Business logic not tied to HTTP
**Scalability:**
- Easy to add new endpoints
- Clear patterns to follow
- Consistent structure
---
## Request Lifecycle
### Complete Flow Example
```typescript
1. HTTP POST /api/users
2. Express matches route in userRoutes.ts
3. Middleware chain executes:
- SSOMiddleware.verifyLoginStatus (authentication)
- auditMiddleware (context tracking)
4. Route handler delegates to controller:
router.post('/users', (req, res) => userController.create(req, res))
5. Controller validates and calls service:
- Validate input with Zod
- Call userService.create(data)
- Handle success/error
6. Service executes business logic:
- Check business rules
- Call userRepository.create(data)
- Return result
7. Repository performs database operation:
- PrismaService.main.user.create({ data })
- Handle database errors
- Return created user
8. Response flows back:
Repository Service Controller Express Client
```
### Middleware Execution Order
**Critical:** Middleware executes in registration order
```typescript
app.use(Sentry.Handlers.requestHandler()); // 1. Sentry tracing (FIRST)
app.use(express.json()); // 2. Body parsing
app.use(express.urlencoded({ extended: true })); // 3. URL encoding
app.use(cookieParser()); // 4. Cookie parsing
app.use(SSOMiddleware.initialize()); // 5. Auth initialization
// ... routes registered here
app.use(auditMiddleware); // 6. Audit (if global)
app.use(errorBoundary); // 7. Error handler (LAST)
app.use(Sentry.Handlers.errorHandler()); // 8. Sentry errors (LAST)
```
**Rule:** Error handlers must be registered AFTER routes!
---
## Service Comparison
### Email Service (Mature Pattern ✅)
**Strengths:**
- Comprehensive BaseController with Sentry integration
- Clean route delegation (no business logic in routes)
- Consistent dependency injection pattern
- Good middleware organization
- Type-safe throughout
- Excellent error handling
**Example Structure:**
```
email/src/
├── controllers/
│ ├── BaseController.ts ✅ Excellent template
│ ├── NotificationController.ts ✅ Extends BaseController
│ └── EmailController.ts ✅ Clean patterns
├── routes/
│ ├── notificationRoutes.ts ✅ Clean delegation
│ └── emailRoutes.ts ✅ No business logic
├── services/
│ ├── NotificationService.ts ✅ Dependency injection
│ └── BatchingService.ts ✅ Clear responsibility
└── middleware/
├── errorBoundary.ts ✅ Comprehensive
└── DevImpersonationSSOMiddleware.ts
```
**Use as template** for new services!
### Form Service (Transitioning ⚠️)
**Strengths:**
- Excellent workflow architecture (event sourcing)
- Good Sentry integration
- Innovative audit middleware (AsyncLocalStorage)
- Comprehensive permission system
**Weaknesses:**
- Some routes have 200+ lines of business logic
- Inconsistent controller naming
- Direct process.env usage (60+ occurrences)
- Minimal repository pattern usage
**Example:**
```
form/src/
├── routes/
│ ├── responseRoutes.ts ❌ Business logic in routes
│ └── proxyRoutes.ts ✅ Good validation pattern
├── controllers/
│ ├── formController.ts ⚠️ Lowercase naming
│ └── UserProfileController.ts ✅ PascalCase naming
├── workflow/ ✅ Excellent architecture!
│ ├── core/
│ │ ├── WorkflowEngineV3.ts ✅ Event sourcing
│ │ └── DryRunWrapper.ts ✅ Innovative
│ └── services/
└── middleware/
└── auditMiddleware.ts ✅ AsyncLocalStorage pattern
```
**Learn from:** workflow/, middleware/auditMiddleware.ts
**Avoid:** responseRoutes.ts, direct process.env
---
## Directory Structure Rationale
### Controllers Directory
**Purpose:** Handle HTTP request/response concerns
**Contents:**
- `BaseController.ts` - Base class with common methods
- `{Feature}Controller.ts` - Feature-specific controllers
**Naming:** PascalCase + Controller
**Responsibilities:**
- Parse request parameters
- Validate input (Zod)
- Call appropriate service methods
- Format responses
- Handle errors (via BaseController)
- Set HTTP status codes
### Services Directory
**Purpose:** Business logic and orchestration
**Contents:**
- `{feature}Service.ts` - Feature business logic
**Naming:** camelCase + Service (or PascalCase + Service)
**Responsibilities:**
- Implement business rules
- Orchestrate multiple repositories
- Transaction management
- Business validations
- No HTTP knowledge (Request/Response types)
### Repositories Directory
**Purpose:** Data access abstraction
**Contents:**
- `{Entity}Repository.ts` - Database operations for entity
**Naming:** PascalCase + Repository
**Responsibilities:**
- Prisma query operations
- Query optimization
- Database error handling
- Caching layer
- Hide Prisma implementation details
**Current Gap:** Only 1 repository exists (WorkflowRepository)
### Routes Directory
**Purpose:** Route registration ONLY
**Contents:**
- `{feature}Routes.ts` - Express router for feature
**Naming:** camelCase + Routes
**Responsibilities:**
- Register routes with Express
- Apply middleware
- Delegate to controllers
- **NO business logic!**
### Middleware Directory
**Purpose:** Cross-cutting concerns
**Contents:**
- Authentication middleware
- Audit middleware
- Error boundaries
- Validation middleware
- Custom middleware
**Naming:** camelCase
**Types:**
- Request processing (before handler)
- Response processing (after handler)
- Error handling (error boundary)
### Config Directory
**Purpose:** Configuration management
**Contents:**
- `unifiedConfig.ts` - Type-safe configuration
- Environment-specific configs
**Pattern:** Single source of truth
### Types Directory
**Purpose:** TypeScript type definitions
**Contents:**
- `{feature}.types.ts` - Feature-specific types
- DTOs (Data Transfer Objects)
- Request/Response types
- Domain models
---
## Module Organization
### Feature-Based Organization
For large features, use subdirectories:
```
src/workflow/
├── core/ # Core engine
├── services/ # Workflow-specific services
├── actions/ # System actions
├── models/ # Domain models
├── validators/ # Workflow validation
└── utils/ # Workflow utilities
```
**When to use:**
- Feature has 5+ files
- Clear sub-domains exist
- Logical grouping improves clarity
### Flat Organization
For simple features:
```
src/
├── controllers/UserController.ts
├── services/userService.ts
├── routes/userRoutes.ts
└── repositories/UserRepository.ts
```
**When to use:**
- Simple features (< 5 files)
- No clear sub-domains
- Flat structure is clearer
---
## Separation of Concerns
### What Goes Where
**Routes Layer:**
- ✅ Route definitions
- ✅ Middleware registration
- ✅ Controller delegation
- ❌ Business logic
- ❌ Database operations
- ❌ Validation logic (should be in validator or controller)
**Controllers Layer:**
- ✅ Request parsing (params, body, query)
- ✅ Input validation (Zod)
- ✅ Service calls
- ✅ Response formatting
- ✅ Error handling
- ❌ Business logic
- ❌ Database operations
**Services Layer:**
- ✅ Business logic
- ✅ Business rules enforcement
- ✅ Orchestration (multiple repos)
- ✅ Transaction management
- ❌ HTTP concerns (Request/Response)
- ❌ Direct Prisma calls (use repositories)
**Repositories Layer:**
- ✅ Prisma operations
- ✅ Query construction
- ✅ Database error handling
- ✅ Caching
- ❌ Business logic
- ❌ HTTP concerns
### Example: User Creation
**Route:**
```typescript
router.post('/users',
SSOMiddleware.verifyLoginStatus,
auditMiddleware,
(req, res) => userController.create(req, res)
);
```
**Controller:**
```typescript
async create(req: Request, res: Response): Promise<void> {
try {
const validated = createUserSchema.parse(req.body);
const user = await this.userService.create(validated);
this.handleSuccess(res, user, 'User created');
} catch (error) {
this.handleError(error, res, 'create');
}
}
```
**Service:**
```typescript
async create(data: CreateUserDTO): Promise<User> {
// Business rule: check if email already exists
const existing = await this.userRepository.findByEmail(data.email);
if (existing) throw new ConflictError('Email already exists');
// Create user
return await this.userRepository.create(data);
}
```
**Repository:**
```typescript
async create(data: CreateUserDTO): Promise<User> {
return PrismaService.main.user.create({ data });
}
async findByEmail(email: string): Promise<User | null> {
return PrismaService.main.user.findUnique({ where: { email } });
}
```
**Notice:** Each layer has clear, distinct responsibilities!
---
**Related Files:**
- SKILL.md - Main guide
- [routing-and-controllers.md](routing-and-controllers.md) - Routes and controllers details
- [services-and-repositories.md](services-and-repositories.md) - Service and repository patterns

View File

@@ -0,0 +1,307 @@
# Async Patterns and Error Handling
Complete guide to async/await patterns and custom error handling.
## Table of Contents
- [Async/Await Best Practices](#asyncawait-best-practices)
- [Promise Error Handling](#promise-error-handling)
- [Custom Error Types](#custom-error-types)
- [asyncErrorWrapper Utility](#asyncerrorwrapper-utility)
- [Error Propagation](#error-propagation)
- [Common Async Pitfalls](#common-async-pitfalls)
---
## Async/Await Best Practices
### Always Use Try-Catch
```typescript
// ❌ NEVER: Unhandled async errors
async function fetchData() {
const data = await database.query(); // If throws, unhandled!
return data;
}
// ✅ ALWAYS: Wrap in try-catch
async function fetchData() {
try {
const data = await database.query();
return data;
} catch (error) {
Sentry.captureException(error);
throw error;
}
}
```
### Avoid .then() Chains
```typescript
// ❌ AVOID: Promise chains
function processData() {
return fetchData()
.then(data => transform(data))
.then(transformed => save(transformed))
.catch(error => {
console.error(error);
});
}
// ✅ PREFER: Async/await
async function processData() {
try {
const data = await fetchData();
const transformed = await transform(data);
return await save(transformed);
} catch (error) {
Sentry.captureException(error);
throw error;
}
}
```
---
## Promise Error Handling
### Parallel Operations
```typescript
// ✅ Handle errors in Promise.all
try {
const [users, profiles, settings] = await Promise.all([
userService.getAll(),
profileService.getAll(),
settingsService.getAll(),
]);
} catch (error) {
// One failure fails all
Sentry.captureException(error);
throw error;
}
// ✅ Handle errors individually with Promise.allSettled
const results = await Promise.allSettled([
userService.getAll(),
profileService.getAll(),
settingsService.getAll(),
]);
results.forEach((result, index) => {
if (result.status === 'rejected') {
Sentry.captureException(result.reason, {
tags: { operation: ['users', 'profiles', 'settings'][index] }
});
}
});
```
---
## Custom Error Types
### Define Custom Errors
```typescript
// Base error class
export class AppError extends Error {
constructor(
message: string,
public code: string,
public statusCode: number,
public isOperational: boolean = true
) {
super(message);
this.name = this.constructor.name;
Error.captureStackTrace(this, this.constructor);
}
}
// Specific error types
export class ValidationError extends AppError {
constructor(message: string) {
super(message, 'VALIDATION_ERROR', 400);
}
}
export class NotFoundError extends AppError {
constructor(message: string) {
super(message, 'NOT_FOUND', 404);
}
}
export class ForbiddenError extends AppError {
constructor(message: string) {
super(message, 'FORBIDDEN', 403);
}
}
export class ConflictError extends AppError {
constructor(message: string) {
super(message, 'CONFLICT', 409);
}
}
```
### Usage
```typescript
// Throw specific errors
if (!user) {
throw new NotFoundError('User not found');
}
if (user.age < 18) {
throw new ValidationError('User must be 18+');
}
// Error boundary handles them
function errorBoundary(error, req, res, next) {
if (error instanceof AppError) {
return res.status(error.statusCode).json({
error: {
message: error.message,
code: error.code
}
});
}
// Unknown error
Sentry.captureException(error);
res.status(500).json({ error: { message: 'Internal server error' } });
}
```
---
## asyncErrorWrapper Utility
### Pattern
```typescript
export function asyncErrorWrapper(
handler: (req: Request, res: Response, next: NextFunction) => Promise<any>
) {
return async (req: Request, res: Response, next: NextFunction) => {
try {
await handler(req, res, next);
} catch (error) {
next(error);
}
};
}
```
### Usage
```typescript
// Without wrapper - error can be unhandled
router.get('/users', async (req, res) => {
const users = await userService.getAll(); // If throws, unhandled!
res.json(users);
});
// With wrapper - errors caught
router.get('/users', asyncErrorWrapper(async (req, res) => {
const users = await userService.getAll();
res.json(users);
}));
```
---
## Error Propagation
### Proper Error Chains
```typescript
// ✅ Propagate errors up the stack
async function repositoryMethod() {
try {
return await PrismaService.main.user.findMany();
} catch (error) {
Sentry.captureException(error, { tags: { layer: 'repository' } });
throw error; // Propagate to service
}
}
async function serviceMethod() {
try {
return await repositoryMethod();
} catch (error) {
Sentry.captureException(error, { tags: { layer: 'service' } });
throw error; // Propagate to controller
}
}
async function controllerMethod(req, res) {
try {
const result = await serviceMethod();
res.json(result);
} catch (error) {
this.handleError(error, res, 'controllerMethod'); // Final handler
}
}
```
---
## Common Async Pitfalls
### Fire and Forget (Bad)
```typescript
// ❌ NEVER: Fire and forget
async function processRequest(req, res) {
sendEmail(user.email); // Fires async, errors unhandled!
res.json({ success: true });
}
// ✅ ALWAYS: Await or handle
async function processRequest(req, res) {
try {
await sendEmail(user.email);
res.json({ success: true });
} catch (error) {
Sentry.captureException(error);
res.status(500).json({ error: 'Failed to send email' });
}
}
// ✅ OR: Intentional background task
async function processRequest(req, res) {
sendEmail(user.email).catch(error => {
Sentry.captureException(error);
});
res.json({ success: true });
}
```
### Unhandled Rejections
```typescript
// ✅ Global handler for unhandled rejections
process.on('unhandledRejection', (reason, promise) => {
Sentry.captureException(reason, {
tags: { type: 'unhandled_rejection' }
});
console.error('Unhandled Rejection:', reason);
});
process.on('uncaughtException', (error) => {
Sentry.captureException(error, {
tags: { type: 'uncaught_exception' }
});
console.error('Uncaught Exception:', error);
process.exit(1);
});
```
---
**Related Files:**
- SKILL.md
- [sentry-and-monitoring.md](sentry-and-monitoring.md)
- [complete-examples.md](complete-examples.md)

View File

@@ -0,0 +1,638 @@
# Complete Examples - Full Working Code
Real-world examples showing complete implementation patterns.
## Table of Contents
- [Complete Controller Example](#complete-controller-example)
- [Complete Service with DI](#complete-service-with-di)
- [Complete Route File](#complete-route-file)
- [Complete Repository](#complete-repository)
- [Refactoring Example: Bad to Good](#refactoring-example-bad-to-good)
- [End-to-End Feature Example](#end-to-end-feature-example)
---
## Complete Controller Example
### UserController (Following All Best Practices)
```typescript
// controllers/UserController.ts
import { Request, Response } from 'express';
import { BaseController } from './BaseController';
import { UserService } from '../services/userService';
import { createUserSchema, updateUserSchema } from '../validators/userSchemas';
import { z } from 'zod';
export class UserController extends BaseController {
private userService: UserService;
constructor() {
super();
this.userService = new UserService();
}
async getUser(req: Request, res: Response): Promise<void> {
try {
this.addBreadcrumb('Fetching user', 'user_controller', {
userId: req.params.id,
});
const user = await this.withTransaction(
'user.get',
'db.query',
() => this.userService.findById(req.params.id)
);
if (!user) {
return this.handleError(
new Error('User not found'),
res,
'getUser',
404
);
}
this.handleSuccess(res, user);
} catch (error) {
this.handleError(error, res, 'getUser');
}
}
async listUsers(req: Request, res: Response): Promise<void> {
try {
const users = await this.userService.getAll();
this.handleSuccess(res, users);
} catch (error) {
this.handleError(error, res, 'listUsers');
}
}
async createUser(req: Request, res: Response): Promise<void> {
try {
// Validate input with Zod
const validated = createUserSchema.parse(req.body);
// Track performance
const user = await this.withTransaction(
'user.create',
'db.mutation',
() => this.userService.create(validated)
);
this.handleSuccess(res, user, 'User created successfully', 201);
} catch (error) {
if (error instanceof z.ZodError) {
return this.handleError(error, res, 'createUser', 400);
}
this.handleError(error, res, 'createUser');
}
}
async updateUser(req: Request, res: Response): Promise<void> {
try {
const validated = updateUserSchema.parse(req.body);
const user = await this.userService.update(
req.params.id,
validated
);
this.handleSuccess(res, user, 'User updated');
} catch (error) {
if (error instanceof z.ZodError) {
return this.handleError(error, res, 'updateUser', 400);
}
this.handleError(error, res, 'updateUser');
}
}
async deleteUser(req: Request, res: Response): Promise<void> {
try {
await this.userService.delete(req.params.id);
this.handleSuccess(res, null, 'User deleted', 204);
} catch (error) {
this.handleError(error, res, 'deleteUser');
}
}
}
```
---
## Complete Service with DI
### UserService
```typescript
// services/userService.ts
import { UserRepository } from '../repositories/UserRepository';
import { ConflictError, NotFoundError, ValidationError } from '../types/errors';
import type { CreateUserDTO, UpdateUserDTO, User } from '../types/user.types';
export class UserService {
private userRepository: UserRepository;
constructor(userRepository?: UserRepository) {
this.userRepository = userRepository || new UserRepository();
}
async findById(id: string): Promise<User | null> {
return await this.userRepository.findById(id);
}
async getAll(): Promise<User[]> {
return await this.userRepository.findActive();
}
async create(data: CreateUserDTO): Promise<User> {
// Business rule: validate age
if (data.age < 18) {
throw new ValidationError('User must be 18 or older');
}
// Business rule: check email uniqueness
const existing = await this.userRepository.findByEmail(data.email);
if (existing) {
throw new ConflictError('Email already in use');
}
// Create user with profile
return await this.userRepository.create({
email: data.email,
profile: {
create: {
firstName: data.firstName,
lastName: data.lastName,
age: data.age,
},
},
});
}
async update(id: string, data: UpdateUserDTO): Promise<User> {
// Check exists
const existing = await this.userRepository.findById(id);
if (!existing) {
throw new NotFoundError('User not found');
}
// Business rule: email uniqueness if changing
if (data.email && data.email !== existing.email) {
const emailTaken = await this.userRepository.findByEmail(data.email);
if (emailTaken) {
throw new ConflictError('Email already in use');
}
}
return await this.userRepository.update(id, data);
}
async delete(id: string): Promise<void> {
const existing = await this.userRepository.findById(id);
if (!existing) {
throw new NotFoundError('User not found');
}
await this.userRepository.delete(id);
}
}
```
---
## Complete Route File
### userRoutes.ts
```typescript
// routes/userRoutes.ts
import { Router } from 'express';
import { UserController } from '../controllers/UserController';
import { SSOMiddlewareClient } from '../middleware/SSOMiddleware';
import { auditMiddleware } from '../middleware/auditMiddleware';
const router = Router();
const controller = new UserController();
// GET /users - List all users
router.get('/',
SSOMiddlewareClient.verifyLoginStatus,
auditMiddleware,
async (req, res) => controller.listUsers(req, res)
);
// GET /users/:id - Get single user
router.get('/:id',
SSOMiddlewareClient.verifyLoginStatus,
auditMiddleware,
async (req, res) => controller.getUser(req, res)
);
// POST /users - Create user
router.post('/',
SSOMiddlewareClient.verifyLoginStatus,
auditMiddleware,
async (req, res) => controller.createUser(req, res)
);
// PUT /users/:id - Update user
router.put('/:id',
SSOMiddlewareClient.verifyLoginStatus,
auditMiddleware,
async (req, res) => controller.updateUser(req, res)
);
// DELETE /users/:id - Delete user
router.delete('/:id',
SSOMiddlewareClient.verifyLoginStatus,
auditMiddleware,
async (req, res) => controller.deleteUser(req, res)
);
export default router;
```
---
## Complete Repository
### UserRepository
```typescript
// repositories/UserRepository.ts
import { PrismaService } from '@project-lifecycle-portal/database';
import type { User, Prisma } from '@prisma/client';
export class UserRepository {
async findById(id: string): Promise<User | null> {
return PrismaService.main.user.findUnique({
where: { id },
include: { profile: true },
});
}
async findByEmail(email: string): Promise<User | null> {
return PrismaService.main.user.findUnique({
where: { email },
include: { profile: true },
});
}
async findActive(): Promise<User[]> {
return PrismaService.main.user.findMany({
where: { isActive: true },
include: { profile: true },
orderBy: { createdAt: 'desc' },
});
}
async create(data: Prisma.UserCreateInput): Promise<User> {
return PrismaService.main.user.create({
data,
include: { profile: true },
});
}
async update(id: string, data: Prisma.UserUpdateInput): Promise<User> {
return PrismaService.main.user.update({
where: { id },
data,
include: { profile: true },
});
}
async delete(id: string): Promise<User> {
// Soft delete
return PrismaService.main.user.update({
where: { id },
data: {
isActive: false,
deletedAt: new Date(),
},
});
}
}
```
---
## Refactoring Example: Bad to Good
### BEFORE: Business Logic in Routes ❌
```typescript
// routes/postRoutes.ts (BAD - 200+ lines)
router.post('/posts', async (req, res) => {
try {
const username = res.locals.claims.preferred_username;
const responses = req.body.responses;
const stepInstanceId = req.body.stepInstanceId;
// ❌ Permission check in route
const userId = await userProfileService.getProfileByEmail(username).then(p => p.id);
const canComplete = await permissionService.canCompleteStep(userId, stepInstanceId);
if (!canComplete) {
return res.status(403).json({ error: 'No permission' });
}
// ❌ Business logic in route
const post = await postRepository.create({
title: req.body.title,
content: req.body.content,
authorId: userId
});
// ❌ More business logic...
if (res.locals.isImpersonating) {
impersonationContextStore.storeContext(...);
}
// ... 100+ more lines
res.json({ success: true, data: result });
} catch (e) {
handler.handleException(res, e);
}
});
```
### AFTER: Clean Separation ✅
**1. Clean Route:**
```typescript
// routes/postRoutes.ts
import { PostController } from '../controllers/PostController';
const router = Router();
const controller = new PostController();
// ✅ CLEAN: 8 lines total!
router.post('/',
SSOMiddlewareClient.verifyLoginStatus,
auditMiddleware,
async (req, res) => controller.createPost(req, res)
);
export default router;
```
**2. Controller:**
```typescript
// controllers/PostController.ts
export class PostController extends BaseController {
private postService: PostService;
constructor() {
super();
this.postService = new PostService();
}
async createPost(req: Request, res: Response): Promise<void> {
try {
const validated = createPostSchema.parse({
...req.body,
});
const result = await this.postService.createPost(
validated,
res.locals.userId
);
this.handleSuccess(res, result, 'Post created successfully');
} catch (error) {
this.handleError(error, res, 'createPost');
}
}
}
```
**3. Service:**
```typescript
// services/postService.ts
export class PostService {
async createPost(
data: CreatePostDTO,
userId: string
): Promise<SubmissionResult> {
// Permission check
const canComplete = await permissionService.canCompleteStep(
userId,
data.stepInstanceId
);
if (!canComplete) {
throw new ForbiddenError('No permission to complete step');
}
// Execute workflow
const engine = await createWorkflowEngine();
const command = new CompleteStepCommand(
data.stepInstanceId,
userId,
data.responses
);
const events = await engine.executeCommand(command);
// Handle impersonation
if (context.isImpersonating) {
await this.handleImpersonation(data.stepInstanceId, context);
}
return { events, success: true };
}
private async handleImpersonation(stepInstanceId: number, context: any) {
impersonationContextStore.storeContext(stepInstanceId, {
originalUserId: context.originalUserId,
effectiveUserId: context.effectiveUserId,
});
}
}
```
**Result:**
- Route: 8 lines (was 200+)
- Controller: 25 lines
- Service: 40 lines
- **Testable, maintainable, reusable!**
---
## End-to-End Feature Example
### Complete User Management Feature
**1. Types:**
```typescript
// types/user.types.ts
export interface User {
id: string;
email: string;
isActive: boolean;
profile?: UserProfile;
}
export interface CreateUserDTO {
email: string;
firstName: string;
lastName: string;
age: number;
}
export interface UpdateUserDTO {
email?: string;
firstName?: string;
lastName?: string;
}
```
**2. Validators:**
```typescript
// validators/userSchemas.ts
import { z } from 'zod';
export const createUserSchema = z.object({
email: z.string().email(),
firstName: z.string().min(1).max(100),
lastName: z.string().min(1).max(100),
age: z.number().int().min(18).max(120),
});
export const updateUserSchema = z.object({
email: z.string().email().optional(),
firstName: z.string().min(1).max(100).optional(),
lastName: z.string().min(1).max(100).optional(),
});
```
**3. Repository:**
```typescript
// repositories/UserRepository.ts
export class UserRepository {
async findById(id: string): Promise<User | null> {
return PrismaService.main.user.findUnique({
where: { id },
include: { profile: true },
});
}
async create(data: Prisma.UserCreateInput): Promise<User> {
return PrismaService.main.user.create({
data,
include: { profile: true },
});
}
}
```
**4. Service:**
```typescript
// services/userService.ts
export class UserService {
private userRepository: UserRepository;
constructor() {
this.userRepository = new UserRepository();
}
async create(data: CreateUserDTO): Promise<User> {
const existing = await this.userRepository.findByEmail(data.email);
if (existing) {
throw new ConflictError('Email already exists');
}
return await this.userRepository.create({
email: data.email,
profile: {
create: {
firstName: data.firstName,
lastName: data.lastName,
age: data.age,
},
},
});
}
}
```
**5. Controller:**
```typescript
// controllers/UserController.ts
export class UserController extends BaseController {
private userService: UserService;
constructor() {
super();
this.userService = new UserService();
}
async createUser(req: Request, res: Response): Promise<void> {
try {
const validated = createUserSchema.parse(req.body);
const user = await this.userService.create(validated);
this.handleSuccess(res, user, 'User created', 201);
} catch (error) {
this.handleError(error, res, 'createUser');
}
}
}
```
**6. Routes:**
```typescript
// routes/userRoutes.ts
const router = Router();
const controller = new UserController();
router.post('/',
SSOMiddlewareClient.verifyLoginStatus,
async (req, res) => controller.createUser(req, res)
);
export default router;
```
**7. Register in app.ts:**
```typescript
// app.ts
import userRoutes from './routes/userRoutes';
app.use('/api/users', userRoutes);
```
**Complete Request Flow:**
```
POST /api/users
userRoutes matches /
SSOMiddleware authenticates
controller.createUser called
Validates with Zod
userService.create called
Checks business rules
userRepository.create called
Prisma creates user
Returns up the chain
Controller formats response
200/201 sent to client
```
---
**Related Files:**
- SKILL.md
- [routing-and-controllers.md](routing-and-controllers.md)
- [services-and-repositories.md](services-and-repositories.md)
- [validation-patterns.md](validation-patterns.md)

View File

@@ -0,0 +1,275 @@
# Configuration Management - UnifiedConfig Pattern
Complete guide to managing configuration in backend microservices.
## Table of Contents
- [UnifiedConfig Overview](#unifiedconfig-overview)
- [NEVER Use process.env Directly](#never-use-processenv-directly)
- [Configuration Structure](#configuration-structure)
- [Environment-Specific Configs](#environment-specific-configs)
- [Secrets Management](#secrets-management)
- [Migration Guide](#migration-guide)
---
## UnifiedConfig Overview
### Why UnifiedConfig?
**Problems with process.env:**
- ❌ No type safety
- ❌ No validation
- ❌ Hard to test
- ❌ Scattered throughout code
- ❌ No default values
- ❌ Runtime errors for typos
**Benefits of unifiedConfig:**
- ✅ Type-safe configuration
- ✅ Single source of truth
- ✅ Validated at startup
- ✅ Easy to test with mocks
- ✅ Clear structure
- ✅ Fallback to environment variables
---
## NEVER Use process.env Directly
### The Rule
```typescript
// ❌ NEVER DO THIS
const timeout = parseInt(process.env.TIMEOUT_MS || '5000');
const dbHost = process.env.DB_HOST || 'localhost';
// ✅ ALWAYS DO THIS
import { config } from './config/unifiedConfig';
const timeout = config.timeouts.default;
const dbHost = config.database.host;
```
### Why This Matters
**Example of problems:**
```typescript
// Typo in environment variable name
const host = process.env.DB_HSOT; // undefined! No error!
// Type safety
const port = process.env.PORT; // string! Need parseInt
const timeout = parseInt(process.env.TIMEOUT); // NaN if not set!
```
**With unifiedConfig:**
```typescript
const port = config.server.port; // number, guaranteed
const timeout = config.timeouts.default; // number, with fallback
```
---
## Configuration Structure
### UnifiedConfig Interface
```typescript
export interface UnifiedConfig {
database: {
host: string;
port: number;
username: string;
password: string;
database: string;
};
server: {
port: number;
sessionSecret: string;
};
tokens: {
jwt: string;
inactivity: string;
internal: string;
};
keycloak: {
realm: string;
client: string;
baseUrl: string;
secret: string;
};
aws: {
region: string;
emailQueueUrl: string;
accessKeyId: string;
secretAccessKey: string;
};
sentry: {
dsn: string;
environment: string;
tracesSampleRate: number;
};
// ... more sections
}
```
### Implementation Pattern
**File:** `/blog-api/src/config/unifiedConfig.ts`
```typescript
import * as fs from 'fs';
import * as path from 'path';
import * as ini from 'ini';
const configPath = path.join(__dirname, '../../config.ini');
const iniConfig = ini.parse(fs.readFileSync(configPath, 'utf-8'));
export const config: UnifiedConfig = {
database: {
host: iniConfig.database?.host || process.env.DB_HOST || 'localhost',
port: parseInt(iniConfig.database?.port || process.env.DB_PORT || '3306'),
username: iniConfig.database?.username || process.env.DB_USER || 'root',
password: iniConfig.database?.password || process.env.DB_PASSWORD || '',
database: iniConfig.database?.database || process.env.DB_NAME || 'blog_dev',
},
server: {
port: parseInt(iniConfig.server?.port || process.env.PORT || '3002'),
sessionSecret: iniConfig.server?.sessionSecret || process.env.SESSION_SECRET || 'dev-secret',
},
// ... more configuration
};
// Validate critical config
if (!config.tokens.jwt) {
throw new Error('JWT secret not configured!');
}
```
**Key Points:**
- Read from config.ini first
- Fallback to process.env
- Default values for development
- Validation at startup
- Type-safe access
---
## Environment-Specific Configs
### config.ini Structure
```ini
[database]
host = localhost
port = 3306
username = root
password = password1
database = blog_dev
[server]
port = 3002
sessionSecret = your-secret-here
[tokens]
jwt = your-jwt-secret
inactivity = 30m
internal = internal-api-token
[keycloak]
realm = myapp
client = myapp-client
baseUrl = http://localhost:8080
secret = keycloak-client-secret
[sentry]
dsn = https://your-sentry-dsn
environment = development
tracesSampleRate = 0.1
```
### Environment Overrides
```bash
# .env file (optional overrides)
DB_HOST=production-db.example.com
DB_PASSWORD=secure-password
PORT=80
```
**Precedence:**
1. config.ini (highest priority)
2. process.env variables
3. Hard-coded defaults (lowest priority)
---
## Secrets Management
### DO NOT Commit Secrets
```gitignore
# .gitignore
config.ini
.env
sentry.ini
*.pem
*.key
```
### Use Environment Variables in Production
```typescript
// Development: config.ini
// Production: Environment variables
export const config: UnifiedConfig = {
database: {
password: process.env.DB_PASSWORD || iniConfig.database?.password || '',
},
tokens: {
jwt: process.env.JWT_SECRET || iniConfig.tokens?.jwt || '',
},
};
```
---
## Migration Guide
### Find All process.env Usage
```bash
grep -r "process.env" blog-api/src/ --include="*.ts" | wc -l
```
### Migration Example
**Before:**
```typescript
// Scattered throughout code
const timeout = parseInt(process.env.OPENID_HTTP_TIMEOUT_MS || '15000');
const keycloakUrl = process.env.KEYCLOAK_BASE_URL;
const jwtSecret = process.env.JWT_SECRET;
```
**After:**
```typescript
import { config } from './config/unifiedConfig';
const timeout = config.keycloak.timeout;
const keycloakUrl = config.keycloak.baseUrl;
const jwtSecret = config.tokens.jwt;
```
**Benefits:**
- Type-safe
- Centralized
- Easy to test
- Validated at startup
---
**Related Files:**
- SKILL.md
- [testing-guide.md](testing-guide.md)

View File

@@ -0,0 +1,224 @@
# Database Patterns - Prisma Best Practices
Complete guide to database access patterns using Prisma in backend microservices.
## Table of Contents
- [PrismaService Usage](#prismaservice-usage)
- [Repository Pattern](#repository-pattern)
- [Transaction Patterns](#transaction-patterns)
- [Query Optimization](#query-optimization)
- [N+1 Query Prevention](#n1-query-prevention)
- [Error Handling](#error-handling)
---
## PrismaService Usage
### Basic Pattern
```typescript
import { PrismaService } from '@project-lifecycle-portal/database';
// Always use PrismaService.main
const users = await PrismaService.main.user.findMany();
```
### Check Availability
```typescript
if (!PrismaService.isAvailable) {
throw new Error('Prisma client not initialized');
}
const user = await PrismaService.main.user.findUnique({ where: { id } });
```
---
## Repository Pattern
### Why Use Repositories
**Use repositories when:**
- Complex queries with joins/includes
- Query used in multiple places
- Need caching layer
- Want to mock for testing
**Skip repositories for:**
- Simple one-off queries
- Prototyping (can refactor later)
### Repository Template
```typescript
export class UserRepository {
async findById(id: string): Promise<User | null> {
return PrismaService.main.user.findUnique({
where: { id },
include: { profile: true },
});
}
async findActive(): Promise<User[]> {
return PrismaService.main.user.findMany({
where: { isActive: true },
orderBy: { createdAt: 'desc' },
});
}
async create(data: Prisma.UserCreateInput): Promise<User> {
return PrismaService.main.user.create({ data });
}
}
```
---
## Transaction Patterns
### Simple Transaction
```typescript
const result = await PrismaService.main.$transaction(async (tx) => {
const user = await tx.user.create({ data: userData });
const profile = await tx.userProfile.create({ data: { userId: user.id } });
return { user, profile };
});
```
### Interactive Transaction
```typescript
const result = await PrismaService.main.$transaction(
async (tx) => {
const user = await tx.user.findUnique({ where: { id } });
if (!user) throw new Error('User not found');
return await tx.user.update({
where: { id },
data: { lastLogin: new Date() },
});
},
{
maxWait: 5000,
timeout: 10000,
}
);
```
---
## Query Optimization
### Use select to Limit Fields
```typescript
// ❌ Fetches all fields
const users = await PrismaService.main.user.findMany();
// ✅ Only fetch needed fields
const users = await PrismaService.main.user.findMany({
select: {
id: true,
email: true,
profile: { select: { firstName: true, lastName: true } },
},
});
```
### Use include Carefully
```typescript
// ❌ Excessive includes
const user = await PrismaService.main.user.findUnique({
where: { id },
include: {
profile: true,
posts: { include: { comments: true } },
workflows: { include: { steps: { include: { actions: true } } } },
},
});
// ✅ Only include what you need
const user = await PrismaService.main.user.findUnique({
where: { id },
include: { profile: true },
});
```
---
## N+1 Query Prevention
### Problem: N+1 Queries
```typescript
// ❌ N+1 Query Problem
const users = await PrismaService.main.user.findMany(); // 1 query
for (const user of users) {
// N queries (one per user)
const profile = await PrismaService.main.userProfile.findUnique({
where: { userId: user.id },
});
}
```
### Solution: Use include or Batching
```typescript
// ✅ Single query with include
const users = await PrismaService.main.user.findMany({
include: { profile: true },
});
// ✅ Or batch query
const userIds = users.map(u => u.id);
const profiles = await PrismaService.main.userProfile.findMany({
where: { userId: { in: userIds } },
});
```
---
## Error Handling
### Prisma Error Types
```typescript
import { Prisma } from '@prisma/client';
try {
await PrismaService.main.user.create({ data });
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
// Unique constraint violation
if (error.code === 'P2002') {
throw new ConflictError('Email already exists');
}
// Foreign key constraint
if (error.code === 'P2003') {
throw new ValidationError('Invalid reference');
}
// Record not found
if (error.code === 'P2025') {
throw new NotFoundError('Record not found');
}
}
// Unknown error
Sentry.captureException(error);
throw error;
}
```
---
**Related Files:**
- SKILL.md
- [services-and-repositories.md](services-and-repositories.md)
- [async-and-errors.md](async-and-errors.md)

View File

@@ -0,0 +1,213 @@
# Middleware Guide - Express Middleware Patterns
Complete guide to creating and using middleware in backend microservices.
## Table of Contents
- [Authentication Middleware](#authentication-middleware)
- [Audit Middleware with AsyncLocalStorage](#audit-middleware-with-asynclocalstorage)
- [Error Boundary Middleware](#error-boundary-middleware)
- [Validation Middleware](#validation-middleware)
- [Composable Middleware](#composable-middleware)
- [Middleware Ordering](#middleware-ordering)
---
## Authentication Middleware
### SSOMiddleware Pattern
**File:** `/form/src/middleware/SSOMiddleware.ts`
```typescript
export class SSOMiddlewareClient {
static verifyLoginStatus(req: Request, res: Response, next: NextFunction): void {
const token = req.cookies.refresh_token;
if (!token) {
return res.status(401).json({ error: 'Not authenticated' });
}
try {
const decoded = jwt.verify(token, config.tokens.jwt);
res.locals.claims = decoded;
res.locals.effectiveUserId = decoded.sub;
next();
} catch (error) {
res.status(401).json({ error: 'Invalid token' });
}
}
}
```
---
## Audit Middleware with AsyncLocalStorage
### Excellent Pattern from Blog API
**File:** `/form/src/middleware/auditMiddleware.ts`
```typescript
import { AsyncLocalStorage } from 'async_hooks';
export interface AuditContext {
userId: string;
userName?: string;
impersonatedBy?: string;
sessionId?: string;
timestamp: Date;
requestId: string;
}
export const auditContextStorage = new AsyncLocalStorage<AuditContext>();
export function auditMiddleware(req: Request, res: Response, next: NextFunction): void {
const context: AuditContext = {
userId: res.locals.effectiveUserId || 'anonymous',
userName: res.locals.claims?.preferred_username,
impersonatedBy: res.locals.isImpersonating ? res.locals.originalUserId : undefined,
timestamp: new Date(),
requestId: req.id || uuidv4(),
};
auditContextStorage.run(context, () => {
next();
});
}
// Getter for current context
export function getAuditContext(): AuditContext | null {
return auditContextStorage.getStore() || null;
}
```
**Benefits:**
- Context propagates through entire request
- No need to pass context through every function
- Automatically available in services, repositories
- Type-safe context access
**Usage in Services:**
```typescript
import { getAuditContext } from '../middleware/auditMiddleware';
async function someOperation() {
const context = getAuditContext();
console.log('Operation by:', context?.userId);
}
```
---
## Error Boundary Middleware
### Comprehensive Error Handler
**File:** `/form/src/middleware/errorBoundary.ts`
```typescript
export function errorBoundary(
error: Error,
req: Request,
res: Response,
next: NextFunction
): void {
// Determine status code
const statusCode = getStatusCodeForError(error);
// Capture to Sentry
Sentry.withScope((scope) => {
scope.setLevel(statusCode >= 500 ? 'error' : 'warning');
scope.setTag('error_type', error.name);
scope.setContext('error_details', {
message: error.message,
stack: error.stack,
});
Sentry.captureException(error);
});
// User-friendly response
res.status(statusCode).json({
success: false,
error: {
message: getUserFriendlyMessage(error),
code: error.name,
},
requestId: Sentry.getCurrentScope().getPropagationContext().traceId,
});
}
// Async wrapper
export function asyncErrorWrapper(
handler: (req: Request, res: Response, next: NextFunction) => Promise<any>
) {
return async (req: Request, res: Response, next: NextFunction) => {
try {
await handler(req, res, next);
} catch (error) {
next(error);
}
};
}
```
---
## Composable Middleware
### withAuthAndAudit Pattern
```typescript
export function withAuthAndAudit(...authMiddleware: any[]) {
return [
...authMiddleware,
auditMiddleware,
];
}
// Usage
router.post('/:formID/submit',
...withAuthAndAudit(SSOMiddlewareClient.verifyLoginStatus),
async (req, res) => controller.submit(req, res)
);
```
---
## Middleware Ordering
### Critical Order (Must Follow)
```typescript
// 1. Sentry request handler (FIRST)
app.use(Sentry.Handlers.requestHandler());
// 2. Body parsing
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// 3. Cookie parsing
app.use(cookieParser());
// 4. Auth initialization
app.use(SSOMiddleware.initialize());
// 5. Routes registered here
app.use('/api/users', userRoutes);
// 6. Error handler (AFTER routes)
app.use(errorBoundary);
// 7. Sentry error handler (LAST)
app.use(Sentry.Handlers.errorHandler());
```
**Rule:** Error handlers MUST be registered AFTER all routes!
---
**Related Files:**
- SKILL.md
- [routing-and-controllers.md](routing-and-controllers.md)
- [async-and-errors.md](async-and-errors.md)

View File

@@ -0,0 +1,756 @@
# Routing and Controllers - Best Practices
Complete guide to clean route definitions and controller patterns.
## Table of Contents
- [Routes: Routing Only](#routes-routing-only)
- [BaseController Pattern](#basecontroller-pattern)
- [Good Examples](#good-examples)
- [Anti-Patterns](#anti-patterns)
- [Refactoring Guide](#refactoring-guide)
- [Error Handling](#error-handling)
- [HTTP Status Codes](#http-status-codes)
---
## Routes: Routing Only
### The Golden Rule
**Routes should ONLY:**
- ✅ Define route paths
- ✅ Register middleware
- ✅ Delegate to controllers
**Routes should NEVER:**
- ❌ Contain business logic
- ❌ Access database directly
- ❌ Implement validation logic (use Zod + controller)
- ❌ Format complex responses
- ❌ Handle complex error scenarios
### Clean Route Pattern
```typescript
// routes/userRoutes.ts
import { Router } from 'express';
import { UserController } from '../controllers/UserController';
import { SSOMiddlewareClient } from '../middleware/SSOMiddleware';
import { auditMiddleware } from '../middleware/auditMiddleware';
const router = Router();
const controller = new UserController();
// ✅ CLEAN: Route definition only
router.get('/:id',
SSOMiddlewareClient.verifyLoginStatus,
auditMiddleware,
async (req, res) => controller.getUser(req, res)
);
router.post('/',
SSOMiddlewareClient.verifyLoginStatus,
auditMiddleware,
async (req, res) => controller.createUser(req, res)
);
router.put('/:id',
SSOMiddlewareClient.verifyLoginStatus,
auditMiddleware,
async (req, res) => controller.updateUser(req, res)
);
export default router;
```
**Key Points:**
- Each route: method, path, middleware chain, controller delegation
- No try-catch needed (controller handles errors)
- Clean, readable, maintainable
- Easy to see all endpoints at a glance
---
## BaseController Pattern
### Why BaseController?
**Benefits:**
- Consistent error handling across all controllers
- Automatic Sentry integration
- Standardized response formats
- Reusable helper methods
- Performance tracking utilities
- Logging and breadcrumb helpers
### BaseController Pattern (Template)
**File:** `/email/src/controllers/BaseController.ts`
```typescript
import * as Sentry from '@sentry/node';
import { Response } from 'express';
export abstract class BaseController {
/**
* Handle errors with Sentry integration
*/
protected handleError(
error: unknown,
res: Response,
context: string,
statusCode = 500
): void {
Sentry.withScope((scope) => {
scope.setTag('controller', this.constructor.name);
scope.setTag('operation', context);
scope.setUser({ id: res.locals?.claims?.userId });
if (error instanceof Error) {
scope.setContext('error_details', {
message: error.message,
stack: error.stack,
});
}
Sentry.captureException(error);
});
res.status(statusCode).json({
success: false,
error: {
message: error instanceof Error ? error.message : 'An error occurred',
code: statusCode,
},
});
}
/**
* Handle success responses
*/
protected handleSuccess<T>(
res: Response,
data: T,
message?: string,
statusCode = 200
): void {
res.status(statusCode).json({
success: true,
message,
data,
});
}
/**
* Performance tracking wrapper
*/
protected async withTransaction<T>(
name: string,
operation: string,
callback: () => Promise<T>
): Promise<T> {
return await Sentry.startSpan(
{ name, op: operation },
callback
);
}
/**
* Validate required fields
*/
protected validateRequest(
required: string[],
actual: Record<string, any>,
res: Response
): boolean {
const missing = required.filter((field) => !actual[field]);
if (missing.length > 0) {
Sentry.captureMessage(
`Missing required fields: ${missing.join(', ')}`,
'warning'
);
res.status(400).json({
success: false,
error: {
message: 'Missing required fields',
code: 'VALIDATION_ERROR',
details: { missing },
},
});
return false;
}
return true;
}
/**
* Logging helpers
*/
protected logInfo(message: string, context?: Record<string, any>): void {
Sentry.addBreadcrumb({
category: this.constructor.name,
message,
level: 'info',
data: context,
});
}
protected logWarning(message: string, context?: Record<string, any>): void {
Sentry.captureMessage(message, {
level: 'warning',
tags: { controller: this.constructor.name },
extra: context,
});
}
/**
* Add Sentry breadcrumb
*/
protected addBreadcrumb(
message: string,
category: string,
data?: Record<string, any>
): void {
Sentry.addBreadcrumb({ message, category, level: 'info', data });
}
/**
* Capture custom metric
*/
protected captureMetric(name: string, value: number, unit: string): void {
Sentry.metrics.gauge(name, value, { unit });
}
}
```
### Using BaseController
```typescript
// controllers/UserController.ts
import { Request, Response } from 'express';
import { BaseController } from './BaseController';
import { UserService } from '../services/userService';
import { createUserSchema } from '../validators/userSchemas';
export class UserController extends BaseController {
private userService: UserService;
constructor() {
super();
this.userService = new UserService();
}
async getUser(req: Request, res: Response): Promise<void> {
try {
this.addBreadcrumb('Fetching user', 'user_controller', { userId: req.params.id });
const user = await this.userService.findById(req.params.id);
if (!user) {
return this.handleError(
new Error('User not found'),
res,
'getUser',
404
);
}
this.handleSuccess(res, user);
} catch (error) {
this.handleError(error, res, 'getUser');
}
}
async createUser(req: Request, res: Response): Promise<void> {
try {
// Validate input
const validated = createUserSchema.parse(req.body);
// Track performance
const user = await this.withTransaction(
'user.create',
'db.query',
() => this.userService.create(validated)
);
this.handleSuccess(res, user, 'User created successfully', 201);
} catch (error) {
this.handleError(error, res, 'createUser');
}
}
async updateUser(req: Request, res: Response): Promise<void> {
try {
const validated = updateUserSchema.parse(req.body);
const user = await this.userService.update(req.params.id, validated);
this.handleSuccess(res, user, 'User updated');
} catch (error) {
this.handleError(error, res, 'updateUser');
}
}
}
```
**Benefits:**
- Consistent error handling
- Automatic Sentry integration
- Performance tracking
- Clean, readable code
- Easy to test
---
## Good Examples
### Example 1: Email Notification Routes (Excellent ✅)
**File:** `/email/src/routes/notificationRoutes.ts`
```typescript
import { Router } from 'express';
import { NotificationController } from '../controllers/NotificationController';
import { SSOMiddlewareClient } from '../middleware/SSOMiddleware';
const router = Router();
const controller = new NotificationController();
// ✅ EXCELLENT: Clean delegation
router.get('/',
SSOMiddlewareClient.verifyLoginStatus,
async (req, res) => controller.getNotifications(req, res)
);
router.post('/',
SSOMiddlewareClient.verifyLoginStatus,
async (req, res) => controller.createNotification(req, res)
);
router.put('/:id/read',
SSOMiddlewareClient.verifyLoginStatus,
async (req, res) => controller.markAsRead(req, res)
);
export default router;
```
**What Makes This Excellent:**
- Zero business logic in routes
- Clear middleware chain
- Consistent pattern
- Easy to understand
### Example 2: Proxy Routes with Validation (Good ✅)
**File:** `/form/src/routes/proxyRoutes.ts`
```typescript
import { z } from 'zod';
const createProxySchema = z.object({
originalUserID: z.string().min(1),
proxyUserID: z.string().min(1),
startsAt: z.string().datetime(),
expiresAt: z.string().datetime(),
});
router.post('/',
SSOMiddlewareClient.verifyLoginStatus,
async (req, res) => {
try {
const validated = createProxySchema.parse(req.body);
const proxy = await proxyService.createProxyRelationship(validated);
res.status(201).json({ success: true, data: proxy });
} catch (error) {
handler.handleException(res, error);
}
}
);
```
**What Makes This Good:**
- Zod validation
- Delegates to service
- Proper HTTP status codes
- Error handling
**Could Be Better:**
- Move validation to controller
- Use BaseController
---
## Anti-Patterns
### Anti-Pattern 1: Business Logic in Routes (Bad ❌)
**File:** `/form/src/routes/responseRoutes.ts` (actual production code)
```typescript
// ❌ ANTI-PATTERN: 200+ lines of business logic in route
router.post('/:formID/submit', async (req: Request, res: Response) => {
try {
const username = res.locals.claims.preferred_username;
const responses = req.body.responses;
const stepInstanceId = req.body.stepInstanceId;
// ❌ Permission checking in route
const userId = await userProfileService.getProfileByEmail(username).then(p => p.id);
const canComplete = await permissionService.canCompleteStep(userId, stepInstanceId);
if (!canComplete) {
return res.status(403).json({ error: 'No permission' });
}
// ❌ Workflow logic in route
const { createWorkflowEngine, CompleteStepCommand } = require('../workflow/core/WorkflowEngineV3');
const engine = await createWorkflowEngine();
const command = new CompleteStepCommand(
stepInstanceId,
userId,
responses,
additionalContext
);
const events = await engine.executeCommand(command);
// ❌ Impersonation handling in route
if (res.locals.isImpersonating) {
impersonationContextStore.storeContext(stepInstanceId, {
originalUserId: res.locals.originalUserId,
effectiveUserId: userId,
});
}
// ❌ Response processing in route
const post = await PrismaService.main.post.findUnique({
where: { id: postData.id },
include: { comments: true },
});
// ❌ Permission check in route
await checkPostPermissions(post, userId);
// ... 100+ more lines of business logic
res.json({ success: true, data: result });
} catch (e) {
handler.handleException(res, e);
}
});
```
**Why This Is Terrible:**
- 200+ lines of business logic
- Hard to test (requires HTTP mocking)
- Hard to reuse (tied to route)
- Mixed responsibilities
- Difficult to debug
- Performance tracking difficult
### How to Refactor (Step-by-Step)
**Step 1: Create Controller**
```typescript
// controllers/PostController.ts
export class PostController extends BaseController {
private postService: PostService;
constructor() {
super();
this.postService = new PostService();
}
async createPost(req: Request, res: Response): Promise<void> {
try {
const validated = createPostSchema.parse({
...req.body,
});
const result = await this.postService.createPost(
validated,
res.locals.userId
);
this.handleSuccess(res, result, 'Post created successfully');
} catch (error) {
this.handleError(error, res, 'createPost');
}
}
}
```
**Step 2: Create Service**
```typescript
// services/postService.ts
export class PostService {
async createPost(
data: CreatePostDTO,
userId: string
): Promise<PostResult> {
// Permission check
const canCreate = await permissionService.canCreatePost(userId);
if (!canCreate) {
throw new ForbiddenError('No permission to create post');
}
// Execute workflow
const engine = await createWorkflowEngine();
const command = new CompleteStepCommand(/* ... */);
const events = await engine.executeCommand(command);
// Handle impersonation if needed
if (context.isImpersonating) {
await this.handleImpersonation(data.stepInstanceId, context);
}
// Synchronize roles
await this.synchronizeRoles(events, userId);
return { events, success: true };
}
private async handleImpersonation(stepInstanceId: number, context: any) {
impersonationContextStore.storeContext(stepInstanceId, {
originalUserId: context.originalUserId,
effectiveUserId: context.effectiveUserId,
});
}
private async synchronizeRoles(events: WorkflowEvent[], userId: string) {
// Role synchronization logic
}
}
```
**Step 3: Update Route**
```typescript
// routes/postRoutes.ts
import { PostController } from '../controllers/PostController';
const router = Router();
const controller = new PostController();
// ✅ CLEAN: Just routing
router.post('/',
SSOMiddlewareClient.verifyLoginStatus,
auditMiddleware,
async (req, res) => controller.createPost(req, res)
);
```
**Result:**
- Route: 8 lines (was 200+)
- Controller: 25 lines (request handling)
- Service: 50 lines (business logic)
- Testable, reusable, maintainable!
---
## Error Handling
### Controller Error Handling
```typescript
async createUser(req: Request, res: Response): Promise<void> {
try {
const result = await this.userService.create(req.body);
this.handleSuccess(res, result, 'User created', 201);
} catch (error) {
// BaseController.handleError automatically:
// - Captures to Sentry with context
// - Sets appropriate status code
// - Returns formatted error response
this.handleError(error, res, 'createUser');
}
}
```
### Custom Error Status Codes
```typescript
async getUser(req: Request, res: Response): Promise<void> {
try {
const user = await this.userService.findById(req.params.id);
if (!user) {
// Custom 404 status
return this.handleError(
new Error('User not found'),
res,
'getUser',
404 // Custom status code
);
}
this.handleSuccess(res, user);
} catch (error) {
this.handleError(error, res, 'getUser');
}
}
```
### Validation Errors
```typescript
async createUser(req: Request, res: Response): Promise<void> {
try {
const validated = createUserSchema.parse(req.body);
const user = await this.userService.create(validated);
this.handleSuccess(res, user, 'User created', 201);
} catch (error) {
// Zod errors get 400 status
if (error instanceof z.ZodError) {
return this.handleError(error, res, 'createUser', 400);
}
this.handleError(error, res, 'createUser');
}
}
```
---
## HTTP Status Codes
### Standard Codes
| Code | Use Case | Example |
|------|----------|---------|
| 200 | Success (GET, PUT) | User retrieved, Updated |
| 201 | Created (POST) | User created |
| 204 | No Content (DELETE) | User deleted |
| 400 | Bad Request | Invalid input data |
| 401 | Unauthorized | Not authenticated |
| 403 | Forbidden | No permission |
| 404 | Not Found | Resource doesn't exist |
| 409 | Conflict | Duplicate resource |
| 422 | Unprocessable Entity | Validation failed |
| 500 | Internal Server Error | Unexpected error |
### Usage Examples
```typescript
// 200 - Success (default)
this.handleSuccess(res, user);
// 201 - Created
this.handleSuccess(res, user, 'Created', 201);
// 400 - Bad Request
this.handleError(error, res, 'operation', 400);
// 404 - Not Found
this.handleError(new Error('Not found'), res, 'operation', 404);
// 403 - Forbidden
this.handleError(new ForbiddenError('No permission'), res, 'operation', 403);
```
---
## Refactoring Guide
### Identify Routes Needing Refactoring
**Red Flags:**
- Route file > 100 lines
- Multiple try-catch blocks in one route
- Direct database access (Prisma calls)
- Complex business logic (if statements, loops)
- Permission checks in routes
**Check your routes:**
```bash
# Find large route files
wc -l form/src/routes/*.ts | sort -n
# Find routes with Prisma usage
grep -r "PrismaService" form/src/routes/
```
### Refactoring Process
**1. Extract to Controller:**
```typescript
// Before: Route with logic
router.post('/action', async (req, res) => {
try {
// 50 lines of logic
} catch (e) {
handler.handleException(res, e);
}
});
// After: Clean route
router.post('/action', (req, res) => controller.performAction(req, res));
// New controller method
async performAction(req: Request, res: Response): Promise<void> {
try {
const result = await this.service.performAction(req.body);
this.handleSuccess(res, result);
} catch (error) {
this.handleError(error, res, 'performAction');
}
}
```
**2. Extract to Service:**
```typescript
// Controller stays thin
async performAction(req: Request, res: Response): Promise<void> {
try {
const validated = actionSchema.parse(req.body);
const result = await this.actionService.execute(validated);
this.handleSuccess(res, result);
} catch (error) {
this.handleError(error, res, 'performAction');
}
}
// Service contains business logic
export class ActionService {
async execute(data: ActionDTO): Promise<Result> {
// All business logic here
// Permission checks
// Database operations
// Complex transformations
return result;
}
}
```
**3. Add Repository (if needed):**
```typescript
// Service calls repository
export class ActionService {
constructor(private actionRepository: ActionRepository) {}
async execute(data: ActionDTO): Promise<Result> {
// Business logic
const entity = await this.actionRepository.findById(data.id);
// More logic
return await this.actionRepository.update(data.id, changes);
}
}
// Repository handles data access
export class ActionRepository {
async findById(id: number): Promise<Entity | null> {
return PrismaService.main.entity.findUnique({ where: { id } });
}
async update(id: number, data: Partial<Entity>): Promise<Entity> {
return PrismaService.main.entity.update({ where: { id }, data });
}
}
```
---
**Related Files:**
- SKILL.md - Main guide
- [services-and-repositories.md](services-and-repositories.md) - Service layer details
- [complete-examples.md](complete-examples.md) - Full refactoring examples

View File

@@ -0,0 +1,336 @@
# Sentry Integration and Monitoring
Complete guide to error tracking and performance monitoring with Sentry v8.
## Table of Contents
- [Core Principles](#core-principles)
- [Sentry Initialization](#sentry-initialization)
- [Error Capture Patterns](#error-capture-patterns)
- [Performance Monitoring](#performance-monitoring)
- [Cron Job Monitoring](#cron-job-monitoring)
- [Error Context Best Practices](#error-context-best-practices)
- [Common Mistakes](#common-mistakes)
---
## Core Principles
**MANDATORY**: All errors MUST be captured to Sentry. No exceptions.
**ALL ERRORS MUST BE CAPTURED** - Use Sentry v8 with comprehensive error tracking across all services.
---
## Sentry Initialization
### instrument.ts Pattern
**Location:** `src/instrument.ts` (MUST be first import in server.ts and all cron jobs)
**Template for Microservices:**
```typescript
import * as Sentry from '@sentry/node';
import * as fs from 'fs';
import * as path from 'path';
import * as ini from 'ini';
const sentryConfigPath = path.join(__dirname, '../sentry.ini');
const sentryConfig = ini.parse(fs.readFileSync(sentryConfigPath, 'utf-8'));
Sentry.init({
dsn: sentryConfig.sentry?.dsn,
environment: process.env.NODE_ENV || 'development',
tracesSampleRate: parseFloat(sentryConfig.sentry?.tracesSampleRate || '0.1'),
profilesSampleRate: parseFloat(sentryConfig.sentry?.profilesSampleRate || '0.1'),
integrations: [
...Sentry.getDefaultIntegrations({}),
Sentry.extraErrorDataIntegration({ depth: 5 }),
Sentry.localVariablesIntegration(),
Sentry.requestDataIntegration({
include: {
cookies: false,
data: true,
headers: true,
ip: true,
query_string: true,
url: true,
user: { id: true, email: true, username: true },
},
}),
Sentry.consoleIntegration(),
Sentry.contextLinesIntegration(),
Sentry.prismaIntegration(),
],
beforeSend(event, hint) {
// Filter health checks
if (event.request?.url?.includes('/healthcheck')) {
return null;
}
// Scrub sensitive headers
if (event.request?.headers) {
delete event.request.headers['authorization'];
delete event.request.headers['cookie'];
}
// Mask emails for PII
if (event.user?.email) {
event.user.email = event.user.email.replace(/^(.{2}).*(@.*)$/, '$1***$2');
}
return event;
},
ignoreErrors: [
/^Invalid JWT/,
/^JWT expired/,
'NetworkError',
],
});
// Set service context
Sentry.setTags({
service: 'form',
version: '1.0.1',
});
Sentry.setContext('runtime', {
node_version: process.version,
platform: process.platform,
});
```
**Critical Points:**
- PII protection built-in (beforeSend)
- Filter non-critical errors
- Comprehensive integrations
- Prisma instrumentation
- Service-specific tagging
---
## Error Capture Patterns
### 1. BaseController Pattern
```typescript
// Use BaseController.handleError
protected handleError(error: unknown, res: Response, context: string, statusCode = 500): void {
Sentry.withScope((scope) => {
scope.setTag('controller', this.constructor.name);
scope.setTag('operation', context);
scope.setUser({ id: res.locals?.claims?.userId });
Sentry.captureException(error);
});
res.status(statusCode).json({
success: false,
error: { message: error instanceof Error ? error.message : 'Error occurred' }
});
}
```
### 2. Workflow Error Handling
```typescript
import { SentryHelper } from '../utils/sentryHelper';
try {
await businessOperation();
} catch (error) {
SentryHelper.captureOperationError(error, {
operationType: 'POST_CREATION',
entityId: 123,
userId: 'user-123',
operation: 'createPost',
});
throw error;
}
```
### 3. Service Layer Error Handling
```typescript
try {
await someOperation();
} catch (error) {
Sentry.captureException(error, {
tags: {
service: 'form',
operation: 'someOperation'
},
extra: {
userId: currentUser.id,
entityId: 123
}
});
throw error;
}
```
---
## Performance Monitoring
### Database Performance Tracking
```typescript
import { DatabasePerformanceMonitor } from '../utils/databasePerformance';
const result = await DatabasePerformanceMonitor.withPerformanceTracking(
'findMany',
'UserProfile',
async () => {
return await PrismaService.main.userProfile.findMany({ take: 5 });
}
);
```
### API Endpoint Spans
```typescript
router.post('/operation', async (req, res) => {
return await Sentry.startSpan({
name: 'operation.execute',
op: 'http.server',
attributes: {
'http.method': 'POST',
'http.route': '/operation'
}
}, async () => {
const result = await performOperation();
res.json(result);
});
});
```
---
## Cron Job Monitoring
### Mandatory Pattern
```typescript
#!/usr/bin/env node
import '../instrument'; // FIRST LINE after shebang
import * as Sentry from '@sentry/node';
async function main() {
return await Sentry.startSpan({
name: 'cron.job-name',
op: 'cron',
attributes: {
'cron.job': 'job-name',
'cron.startTime': new Date().toISOString(),
}
}, async () => {
try {
// Cron job logic here
} catch (error) {
Sentry.captureException(error, {
tags: {
'cron.job': 'job-name',
'error.type': 'execution_error'
}
});
console.error('[Cron] Error:', error);
process.exit(1);
}
});
}
main().then(() => {
console.log('[Cron] Completed successfully');
process.exit(0);
}).catch((error) => {
console.error('[Cron] Fatal error:', error);
process.exit(1);
});
```
---
## Error Context Best Practices
### Rich Context Example
```typescript
Sentry.withScope((scope) => {
// User context
scope.setUser({
id: user.id,
email: user.email,
username: user.username
});
// Tags for filtering
scope.setTag('service', 'form');
scope.setTag('endpoint', req.path);
scope.setTag('method', req.method);
// Structured context
scope.setContext('operation', {
type: 'workflow.complete',
workflowId: 123,
stepId: 456
});
// Breadcrumbs for timeline
scope.addBreadcrumb({
category: 'workflow',
message: 'Starting step completion',
level: 'info',
data: { stepId: 456 }
});
Sentry.captureException(error);
});
```
---
## Common Mistakes
```typescript
// ❌ Swallowing errors
try {
await riskyOperation();
} catch (error) {
// Silent failure
}
// ❌ Generic error messages
throw new Error('Error occurred');
// ❌ Exposing sensitive data
Sentry.captureException(error, {
extra: { password: user.password } // NEVER
});
// ❌ Missing async error handling
async function bad() {
fetchData().then(data => processResult(data)); // Unhandled
}
// ✅ Proper async handling
async function good() {
try {
const data = await fetchData();
processResult(data);
} catch (error) {
Sentry.captureException(error);
throw error;
}
}
```
---
**Related Files:**
- SKILL.md
- [routing-and-controllers.md](routing-and-controllers.md)
- [async-and-errors.md](async-and-errors.md)

View File

@@ -0,0 +1,789 @@
# Services and Repositories - Business Logic Layer
Complete guide to organizing business logic with services and data access with repositories.
## Table of Contents
- [Service Layer Overview](#service-layer-overview)
- [Dependency Injection Pattern](#dependency-injection-pattern)
- [Singleton Pattern](#singleton-pattern)
- [Repository Pattern](#repository-pattern)
- [Service Design Principles](#service-design-principles)
- [Caching Strategies](#caching-strategies)
- [Testing Services](#testing-services)
---
## Service Layer Overview
### Purpose of Services
**Services contain business logic** - the 'what' and 'why' of your application:
```
Controller asks: "Should I do this?"
Service answers: "Yes/No, here's why, and here's what happens"
Repository executes: "Here's the data you requested"
```
**Services are responsible for:**
- ✅ Business rules enforcement
- ✅ Orchestrating multiple repositories
- ✅ Transaction management
- ✅ Complex calculations
- ✅ External service integration
- ✅ Business validations
**Services should NOT:**
- ❌ Know about HTTP (Request/Response)
- ❌ Direct Prisma access (use repositories)
- ❌ Handle route-specific logic
- ❌ Format HTTP responses
---
## Dependency Injection Pattern
### Why Dependency Injection?
**Benefits:**
- Easy to test (inject mocks)
- Clear dependencies
- Flexible configuration
- Promotes loose coupling
### Excellent Example: NotificationService
**File:** `/blog-api/src/services/NotificationService.ts`
```typescript
// Define dependencies interface for clarity
export interface NotificationServiceDependencies {
prisma: PrismaClient;
batchingService: BatchingService;
emailComposer: EmailComposer;
}
// Service with dependency injection
export class NotificationService {
private prisma: PrismaClient;
private batchingService: BatchingService;
private emailComposer: EmailComposer;
private preferencesCache: Map<string, { preferences: UserPreference; timestamp: number }> = new Map();
private CACHE_TTL = (notificationConfig.preferenceCacheTTLMinutes || 5) * 60 * 1000;
// Dependencies injected via constructor
constructor(dependencies: NotificationServiceDependencies) {
this.prisma = dependencies.prisma;
this.batchingService = dependencies.batchingService;
this.emailComposer = dependencies.emailComposer;
}
/**
* Create a notification and route it appropriately
*/
async createNotification(params: CreateNotificationParams) {
const { recipientID, type, title, message, link, context = {}, channel = 'both', priority = NotificationPriority.NORMAL } = params;
try {
// Get template and render content
const template = getNotificationTemplate(type);
const rendered = renderNotificationContent(template, context);
// Create in-app notification record
const notificationId = await createNotificationRecord({
instanceId: parseInt(context.instanceId || '0', 10),
template: type,
recipientUserId: recipientID,
channel: channel === 'email' ? 'email' : 'inApp',
contextData: context,
title: finalTitle,
message: finalMessage,
link: finalLink,
});
// Route notification based on channel
if (channel === 'email' || channel === 'both') {
await this.routeNotification({
notificationId,
userId: recipientID,
type,
priority,
title: finalTitle,
message: finalMessage,
link: finalLink,
context,
});
}
return notification;
} catch (error) {
ErrorLogger.log(error, {
context: {
'[NotificationService] createNotification': {
type: params.type,
recipientID: params.recipientID,
},
},
});
throw error;
}
}
/**
* Route notification based on user preferences
*/
private async routeNotification(params: { notificationId: number; userId: string; type: string; priority: NotificationPriority; title: string; message: string; link?: string; context?: Record<string, any> }) {
// Get user preferences with caching
const preferences = await this.getUserPreferences(params.userId);
// Check if we should batch or send immediately
if (this.shouldBatchEmail(preferences, params.type, params.priority)) {
await this.batchingService.queueNotificationForBatch({
notificationId: params.notificationId,
userId: params.userId,
userPreference: preferences,
priority: params.priority,
});
} else {
// Send immediately via EmailComposer
await this.sendImmediateEmail({
userId: params.userId,
title: params.title,
message: params.message,
link: params.link,
context: params.context,
type: params.type,
});
}
}
/**
* Determine if email should be batched
*/
shouldBatchEmail(preferences: UserPreference, notificationType: string, priority: NotificationPriority): boolean {
// HIGH priority always immediate
if (priority === NotificationPriority.HIGH) {
return false;
}
// Check batch mode
const batchMode = preferences.emailBatchMode || BatchMode.IMMEDIATE;
return batchMode !== BatchMode.IMMEDIATE;
}
/**
* Get user preferences with caching
*/
async getUserPreferences(userId: string): Promise<UserPreference> {
// Check cache first
const cached = this.preferencesCache.get(userId);
if (cached && Date.now() - cached.timestamp < this.CACHE_TTL) {
return cached.preferences;
}
const preference = await this.prisma.userPreference.findUnique({
where: { userID: userId },
});
const finalPreferences = preference || DEFAULT_PREFERENCES;
// Update cache
this.preferencesCache.set(userId, {
preferences: finalPreferences,
timestamp: Date.now(),
});
return finalPreferences;
}
}
```
**Usage in Controller:**
```typescript
// Instantiate with dependencies
const notificationService = new NotificationService({
prisma: PrismaService.main,
batchingService: new BatchingService(PrismaService.main),
emailComposer: new EmailComposer(),
});
// Use in controller
const notification = await notificationService.createNotification({
recipientID: 'user-123',
type: 'AFRLWorkflowNotification',
context: { workflowName: 'AFRL Monthly Report' },
});
```
**Key Takeaways:**
- Dependencies passed via constructor
- Clear interface defines required dependencies
- Easy to test (inject mocks)
- Encapsulated caching logic
- Business rules isolated from HTTP
---
## Singleton Pattern
### When to Use Singletons
**Use for:**
- Services with expensive initialization
- Services with shared state (caching)
- Services accessed from many places
- Permission services
- Configuration services
### Example: PermissionService (Singleton)
**File:** `/blog-api/src/services/permissionService.ts`
```typescript
import { PrismaClient } from '@prisma/client';
class PermissionService {
private static instance: PermissionService;
private prisma: PrismaClient;
private permissionCache: Map<string, { canAccess: boolean; timestamp: number }> = new Map();
private CACHE_TTL = 5 * 60 * 1000; // 5 minutes
// Private constructor prevents direct instantiation
private constructor() {
this.prisma = PrismaService.main;
}
// Get singleton instance
public static getInstance(): PermissionService {
if (!PermissionService.instance) {
PermissionService.instance = new PermissionService();
}
return PermissionService.instance;
}
/**
* Check if user can complete a workflow step
*/
async canCompleteStep(userId: string, stepInstanceId: number): Promise<boolean> {
const cacheKey = `${userId}:${stepInstanceId}`;
// Check cache
const cached = this.permissionCache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < this.CACHE_TTL) {
return cached.canAccess;
}
try {
const post = await this.prisma.post.findUnique({
where: { id: postId },
include: {
author: true,
comments: {
include: {
user: true,
},
},
},
});
if (!post) {
return false;
}
// Check if user has permission
const canEdit = post.authorId === userId ||
await this.isUserAdmin(userId);
// Cache result
this.permissionCache.set(cacheKey, {
canAccess: isAssigned,
timestamp: Date.now(),
});
return isAssigned;
} catch (error) {
console.error('[PermissionService] Error checking step permission:', error);
return false;
}
}
/**
* Clear cache for user
*/
clearUserCache(userId: string): void {
for (const [key] of this.permissionCache) {
if (key.startsWith(`${userId}:`)) {
this.permissionCache.delete(key);
}
}
}
/**
* Clear all cache
*/
clearCache(): void {
this.permissionCache.clear();
}
}
// Export singleton instance
export const permissionService = PermissionService.getInstance();
```
**Usage:**
```typescript
import { permissionService } from '../services/permissionService';
// Use anywhere in the codebase
const canComplete = await permissionService.canCompleteStep(userId, stepId);
if (!canComplete) {
throw new ForbiddenError('You do not have permission to complete this step');
}
```
---
## Repository Pattern
### Purpose of Repositories
**Repositories abstract data access** - the 'how' of data operations:
```
Service: "Get me all active users sorted by name"
Repository: "Here's the Prisma query that does that"
```
**Repositories are responsible for:**
- ✅ All Prisma operations
- ✅ Query construction
- ✅ Query optimization (select, include)
- ✅ Database error handling
- ✅ Caching database results
**Repositories should NOT:**
- ❌ Contain business logic
- ❌ Know about HTTP
- ❌ Make decisions (that's service layer)
### Repository Template
```typescript
// repositories/UserRepository.ts
import { PrismaService } from '@project-lifecycle-portal/database';
import type { User, Prisma } from '@project-lifecycle-portal/database';
export class UserRepository {
/**
* Find user by ID with optimized query
*/
async findById(userId: string): Promise<User | null> {
try {
return await PrismaService.main.user.findUnique({
where: { userID: userId },
select: {
userID: true,
email: true,
name: true,
isActive: true,
roles: true,
createdAt: true,
updatedAt: true,
},
});
} catch (error) {
console.error('[UserRepository] Error finding user by ID:', error);
throw new Error(`Failed to find user: ${userId}`);
}
}
/**
* Find all active users
*/
async findActive(options?: { orderBy?: Prisma.UserOrderByWithRelationInput }): Promise<User[]> {
try {
return await PrismaService.main.user.findMany({
where: { isActive: true },
orderBy: options?.orderBy || { name: 'asc' },
select: {
userID: true,
email: true,
name: true,
roles: true,
},
});
} catch (error) {
console.error('[UserRepository] Error finding active users:', error);
throw new Error('Failed to find active users');
}
}
/**
* Find user by email
*/
async findByEmail(email: string): Promise<User | null> {
try {
return await PrismaService.main.user.findUnique({
where: { email },
});
} catch (error) {
console.error('[UserRepository] Error finding user by email:', error);
throw new Error(`Failed to find user with email: ${email}`);
}
}
/**
* Create new user
*/
async create(data: Prisma.UserCreateInput): Promise<User> {
try {
return await PrismaService.main.user.create({ data });
} catch (error) {
console.error('[UserRepository] Error creating user:', error);
throw new Error('Failed to create user');
}
}
/**
* Update user
*/
async update(userId: string, data: Prisma.UserUpdateInput): Promise<User> {
try {
return await PrismaService.main.user.update({
where: { userID: userId },
data,
});
} catch (error) {
console.error('[UserRepository] Error updating user:', error);
throw new Error(`Failed to update user: ${userId}`);
}
}
/**
* Delete user (soft delete by setting isActive = false)
*/
async delete(userId: string): Promise<User> {
try {
return await PrismaService.main.user.update({
where: { userID: userId },
data: { isActive: false },
});
} catch (error) {
console.error('[UserRepository] Error deleting user:', error);
throw new Error(`Failed to delete user: ${userId}`);
}
}
/**
* Check if email exists
*/
async emailExists(email: string): Promise<boolean> {
try {
const count = await PrismaService.main.user.count({
where: { email },
});
return count > 0;
} catch (error) {
console.error('[UserRepository] Error checking email exists:', error);
throw new Error('Failed to check if email exists');
}
}
}
// Export singleton instance
export const userRepository = new UserRepository();
```
**Using Repository in Service:**
```typescript
// services/userService.ts
import { userRepository } from '../repositories/UserRepository';
import { ConflictError, NotFoundError } from '../utils/errors';
export class UserService {
/**
* Create new user with business rules
*/
async createUser(data: { email: string; name: string; roles: string[] }): Promise<User> {
// Business rule: Check if email already exists
const emailExists = await userRepository.emailExists(data.email);
if (emailExists) {
throw new ConflictError('Email already exists');
}
// Business rule: Validate roles
const validRoles = ['admin', 'operations', 'user'];
const invalidRoles = data.roles.filter((role) => !validRoles.includes(role));
if (invalidRoles.length > 0) {
throw new ValidationError(`Invalid roles: ${invalidRoles.join(', ')}`);
}
// Create user via repository
return await userRepository.create({
email: data.email,
name: data.name,
roles: data.roles,
isActive: true,
});
}
/**
* Get user by ID
*/
async getUser(userId: string): Promise<User> {
const user = await userRepository.findById(userId);
if (!user) {
throw new NotFoundError(`User not found: ${userId}`);
}
return user;
}
}
```
---
## Service Design Principles
### 1. Single Responsibility
Each service should have ONE clear purpose:
```typescript
// ✅ GOOD - Single responsibility
class UserService {
async createUser() {}
async updateUser() {}
async deleteUser() {}
}
class EmailService {
async sendEmail() {}
async sendBulkEmails() {}
}
// ❌ BAD - Too many responsibilities
class UserService {
async createUser() {}
async sendWelcomeEmail() {} // Should be EmailService
async logUserActivity() {} // Should be AuditService
async processPayment() {} // Should be PaymentService
}
```
### 2. Clear Method Names
Method names should describe WHAT they do:
```typescript
// ✅ GOOD - Clear intent
async createNotification()
async getUserPreferences()
async shouldBatchEmail()
async routeNotification()
// ❌ BAD - Vague or misleading
async process()
async handle()
async doIt()
async execute()
```
### 3. Return Types
Always use explicit return types:
```typescript
// ✅ GOOD - Explicit types
async createUser(data: CreateUserDTO): Promise<User> {}
async findUsers(): Promise<User[]> {}
async deleteUser(id: string): Promise<void> {}
// ❌ BAD - Implicit any
async createUser(data) {} // No types!
```
### 4. Error Handling
Services should throw meaningful errors:
```typescript
// ✅ GOOD - Meaningful errors
if (!user) {
throw new NotFoundError(`User not found: ${userId}`);
}
if (emailExists) {
throw new ConflictError('Email already exists');
}
// ❌ BAD - Generic errors
if (!user) {
throw new Error('Error'); // What error?
}
```
### 5. Avoid God Services
Don't create services that do everything:
```typescript
// ❌ BAD - God service
class WorkflowService {
async startWorkflow() {}
async completeStep() {}
async assignRoles() {}
async sendNotifications() {} // Should be NotificationService
async validatePermissions() {} // Should be PermissionService
async logAuditTrail() {} // Should be AuditService
// ... 50 more methods
}
// ✅ GOOD - Focused services
class WorkflowService {
constructor(
private notificationService: NotificationService,
private permissionService: PermissionService,
private auditService: AuditService
) {}
async startWorkflow() {
// Orchestrate other services
await this.permissionService.checkPermission();
await this.workflowRepository.create();
await this.notificationService.notify();
await this.auditService.log();
}
}
```
---
## Caching Strategies
### 1. In-Memory Caching
```typescript
class UserService {
private cache: Map<string, { user: User; timestamp: number }> = new Map();
private CACHE_TTL = 5 * 60 * 1000; // 5 minutes
async getUser(userId: string): Promise<User> {
// Check cache
const cached = this.cache.get(userId);
if (cached && Date.now() - cached.timestamp < this.CACHE_TTL) {
return cached.user;
}
// Fetch from database
const user = await userRepository.findById(userId);
// Update cache
if (user) {
this.cache.set(userId, { user, timestamp: Date.now() });
}
return user;
}
clearUserCache(userId: string): void {
this.cache.delete(userId);
}
}
```
### 2. Cache Invalidation
```typescript
class UserService {
async updateUser(userId: string, data: UpdateUserDTO): Promise<User> {
// Update in database
const user = await userRepository.update(userId, data);
// Invalidate cache
this.clearUserCache(userId);
return user;
}
}
```
---
## Testing Services
### Unit Tests
```typescript
// tests/userService.test.ts
import { UserService } from '../services/userService';
import { userRepository } from '../repositories/UserRepository';
import { ConflictError } from '../utils/errors';
// Mock repository
jest.mock('../repositories/UserRepository');
describe('UserService', () => {
let userService: UserService;
beforeEach(() => {
userService = new UserService();
jest.clearAllMocks();
});
describe('createUser', () => {
it('should create user when email does not exist', async () => {
// Arrange
const userData = {
email: 'test@example.com',
name: 'Test User',
roles: ['user'],
};
(userRepository.emailExists as jest.Mock).mockResolvedValue(false);
(userRepository.create as jest.Mock).mockResolvedValue({
userID: '123',
...userData,
});
// Act
const user = await userService.createUser(userData);
// Assert
expect(user).toBeDefined();
expect(user.email).toBe(userData.email);
expect(userRepository.emailExists).toHaveBeenCalledWith(userData.email);
expect(userRepository.create).toHaveBeenCalled();
});
it('should throw ConflictError when email exists', async () => {
// Arrange
const userData = {
email: 'existing@example.com',
name: 'Test User',
roles: ['user'],
};
(userRepository.emailExists as jest.Mock).mockResolvedValue(true);
// Act & Assert
await expect(userService.createUser(userData)).rejects.toThrow(ConflictError);
expect(userRepository.create).not.toHaveBeenCalled();
});
});
});
```
---
**Related Files:**
- SKILL.md - Main guide
- [routing-and-controllers.md](routing-and-controllers.md) - Controllers that use services
- [database-patterns.md](database-patterns.md) - Prisma and repository patterns
- [complete-examples.md](complete-examples.md) - Full service/repository examples

View File

@@ -0,0 +1,235 @@
# Testing Guide - Backend Testing Strategies
Complete guide to testing backend services with Jest and best practices.
## Table of Contents
- [Unit Testing](#unit-testing)
- [Integration Testing](#integration-testing)
- [Mocking Strategies](#mocking-strategies)
- [Test Data Management](#test-data-management)
- [Testing Authenticated Routes](#testing-authenticated-routes)
- [Coverage Targets](#coverage-targets)
---
## Unit Testing
### Test Structure
```typescript
// services/userService.test.ts
import { UserService } from './userService';
import { UserRepository } from '../repositories/UserRepository';
jest.mock('../repositories/UserRepository');
describe('UserService', () => {
let service: UserService;
let mockRepository: jest.Mocked<UserRepository>;
beforeEach(() => {
mockRepository = {
findByEmail: jest.fn(),
create: jest.fn(),
} as any;
service = new UserService();
(service as any).userRepository = mockRepository;
});
afterEach(() => {
jest.clearAllMocks();
});
describe('create', () => {
it('should throw error if email exists', async () => {
mockRepository.findByEmail.mockResolvedValue({ id: '123' } as any);
await expect(
service.create({ email: 'test@test.com' })
).rejects.toThrow('Email already in use');
});
it('should create user if email is unique', async () => {
mockRepository.findByEmail.mockResolvedValue(null);
mockRepository.create.mockResolvedValue({ id: '123' } as any);
const user = await service.create({
email: 'test@test.com',
firstName: 'John',
lastName: 'Doe',
});
expect(user).toBeDefined();
expect(mockRepository.create).toHaveBeenCalledWith(
expect.objectContaining({
email: 'test@test.com'
})
);
});
});
});
```
---
## Integration Testing
### Test with Real Database
```typescript
import { PrismaService } from '@project-lifecycle-portal/database';
describe('UserService Integration', () => {
let testUser: any;
beforeAll(async () => {
// Create test data
testUser = await PrismaService.main.user.create({
data: {
email: 'test@test.com',
profile: { create: { firstName: 'Test', lastName: 'User' } },
},
});
});
afterAll(async () => {
// Cleanup
await PrismaService.main.user.delete({ where: { id: testUser.id } });
});
it('should find user by email', async () => {
const user = await userService.findByEmail('test@test.com');
expect(user).toBeDefined();
expect(user?.email).toBe('test@test.com');
});
});
```
---
## Mocking Strategies
### Mock PrismaService
```typescript
jest.mock('@project-lifecycle-portal/database', () => ({
PrismaService: {
main: {
user: {
findMany: jest.fn(),
findUnique: jest.fn(),
create: jest.fn(),
update: jest.fn(),
},
},
isAvailable: true,
},
}));
```
### Mock Services
```typescript
const mockUserService = {
findById: jest.fn(),
create: jest.fn(),
update: jest.fn(),
} as jest.Mocked<UserService>;
```
---
## Test Data Management
### Setup and Teardown
```typescript
describe('PermissionService', () => {
let instanceId: number;
beforeAll(async () => {
// Create test post
const post = await PrismaService.main.post.create({
data: { title: 'Test Post', content: 'Test', authorId: 'test-user' },
});
instanceId = post.id;
});
afterAll(async () => {
// Cleanup
await PrismaService.main.post.delete({
where: { id: instanceId },
});
});
beforeEach(() => {
// Clear caches
permissionService.clearCache();
});
it('should check permissions', async () => {
const hasPermission = await permissionService.checkPermission(
'user-id',
instanceId,
'VIEW_WORKFLOW'
);
expect(hasPermission).toBeDefined();
});
});
```
---
## Testing Authenticated Routes
### Using test-auth-route.js
```bash
# Test authenticated endpoint
node scripts/test-auth-route.js http://localhost:3002/form/api/users
# Test with POST data
node scripts/test-auth-route.js http://localhost:3002/form/api/users POST '{"email":"test@test.com"}'
```
### Mock Authentication in Tests
```typescript
// Mock auth middleware
jest.mock('../middleware/SSOMiddleware', () => ({
SSOMiddlewareClient: {
verifyLoginStatus: (req, res, next) => {
res.locals.claims = {
sub: 'test-user-id',
preferred_username: 'testuser',
};
next();
},
},
}));
```
---
## Coverage Targets
### Recommended Coverage
- **Unit Tests**: 70%+ coverage
- **Integration Tests**: Critical paths covered
- **E2E Tests**: Happy paths covered
### Run Coverage
```bash
npm test -- --coverage
```
---
**Related Files:**
- SKILL.md
- [services-and-repositories.md](services-and-repositories.md)
- [complete-examples.md](complete-examples.md)

View File

@@ -0,0 +1,754 @@
# Validation Patterns - Input Validation with Zod
Complete guide to input validation using Zod schemas for type-safe validation.
## Table of Contents
- [Why Zod?](#why-zod)
- [Basic Zod Patterns](#basic-zod-patterns)
- [Schema Examples from Codebase](#schema-examples-from-codebase)
- [Route-Level Validation](#route-level-validation)
- [Controller Validation](#controller-validation)
- [DTO Pattern](#dto-pattern)
- [Error Handling](#error-handling)
- [Advanced Patterns](#advanced-patterns)
---
## Why Zod?
### Benefits Over Joi/Other Libraries
**Type Safety:**
- ✅ Full TypeScript inference
- ✅ Runtime + compile-time validation
- ✅ Automatic type generation
**Developer Experience:**
- ✅ Intuitive API
- ✅ Composable schemas
- ✅ Excellent error messages
**Performance:**
- ✅ Fast validation
- ✅ Small bundle size
- ✅ Tree-shakeable
### Migration from Joi
Modern validation uses Zod instead of Joi:
```typescript
// ❌ OLD - Joi (being phased out)
const schema = Joi.object({
email: Joi.string().email().required(),
name: Joi.string().min(3).required(),
});
// ✅ NEW - Zod (preferred)
const schema = z.object({
email: z.string().email(),
name: z.string().min(3),
});
```
---
## Basic Zod Patterns
### Primitive Types
```typescript
import { z } from 'zod';
// Strings
const nameSchema = z.string();
const emailSchema = z.string().email();
const urlSchema = z.string().url();
const uuidSchema = z.string().uuid();
const minLengthSchema = z.string().min(3);
const maxLengthSchema = z.string().max(100);
// Numbers
const ageSchema = z.number().int().positive();
const priceSchema = z.number().positive();
const rangeSchema = z.number().min(0).max(100);
// Booleans
const activeSchema = z.boolean();
// Dates
const dateSchema = z.string().datetime(); // ISO 8601 string
const nativeDateSchema = z.date(); // Native Date object
// Enums
const roleSchema = z.enum(['admin', 'operations', 'user']);
const statusSchema = z.enum(['PENDING', 'APPROVED', 'REJECTED']);
```
### Objects
```typescript
// Simple object
const userSchema = z.object({
email: z.string().email(),
name: z.string(),
age: z.number().int().positive(),
});
// Nested objects
const addressSchema = z.object({
street: z.string(),
city: z.string(),
zipCode: z.string().regex(/^\d{5}$/),
});
const userWithAddressSchema = z.object({
name: z.string(),
address: addressSchema,
});
// Optional fields
const userSchema = z.object({
name: z.string(),
email: z.string().email().optional(),
phone: z.string().optional(),
});
// Nullable fields
const userSchema = z.object({
name: z.string(),
middleName: z.string().nullable(),
});
```
### Arrays
```typescript
// Array of primitives
const rolesSchema = z.array(z.string());
const numbersSchema = z.array(z.number());
// Array of objects
const usersSchema = z.array(
z.object({
id: z.string(),
name: z.string(),
})
);
// Array with constraints
const tagsSchema = z.array(z.string()).min(1).max(10);
const nonEmptyArray = z.array(z.string()).nonempty();
```
---
## Schema Examples from Codebase
### Form Validation Schemas
**File:** `/form/src/helpers/zodSchemas.ts`
```typescript
import { z } from 'zod';
// Question types enum
export const questionTypeSchema = z.enum([
'input',
'textbox',
'editor',
'dropdown',
'autocomplete',
'checkbox',
'radio',
'upload',
]);
// Upload types
export const uploadTypeSchema = z.array(
z.enum(['pdf', 'image', 'excel', 'video', 'powerpoint', 'word']).nullable()
);
// Input types
export const inputTypeSchema = z
.enum(['date', 'number', 'input', 'currency'])
.nullable();
// Question option
export const questionOptionSchema = z.object({
id: z.number().int().positive().optional(),
controlTag: z.string().max(150).nullable().optional(),
label: z.string().max(100).nullable().optional(),
order: z.number().int().min(0).default(0),
});
// Question schema
export const questionSchema = z.object({
id: z.number().int().positive().optional(),
formID: z.number().int().positive(),
sectionID: z.number().int().positive().optional(),
options: z.array(questionOptionSchema).optional(),
label: z.string().max(500),
description: z.string().max(5000).optional(),
type: questionTypeSchema,
uploadTypes: uploadTypeSchema.optional(),
inputType: inputTypeSchema.optional(),
tags: z.array(z.string().max(150)).optional(),
required: z.boolean(),
isStandard: z.boolean().optional(),
deprecatedKey: z.string().nullable().optional(),
maxLength: z.number().int().positive().nullable().optional(),
isOptionsSorted: z.boolean().optional(),
});
// Form section schema
export const formSectionSchema = z.object({
id: z.number().int().positive(),
formID: z.number().int().positive(),
questions: z.array(questionSchema).optional(),
label: z.string().max(500),
description: z.string().max(5000).optional(),
isStandard: z.boolean(),
});
// Create form schema
export const createFormSchema = z.object({
id: z.number().int().positive(),
label: z.string().max(150),
description: z.string().max(6000).nullable().optional(),
isPhase: z.boolean().optional(),
username: z.string(),
});
// Update order schema
export const updateOrderSchema = z.object({
source: z.object({
index: z.number().int().min(0),
sectionID: z.number().int().min(0),
}),
destination: z.object({
index: z.number().int().min(0),
sectionID: z.number().int().min(0),
}),
});
// Controller-specific validation schemas
export const createQuestionValidationSchema = z.object({
formID: z.number().int().positive(),
sectionID: z.number().int().positive(),
question: questionSchema,
index: z.number().int().min(0).nullable().optional(),
username: z.string(),
});
export const updateQuestionValidationSchema = z.object({
questionID: z.number().int().positive(),
username: z.string(),
question: questionSchema,
});
```
### Proxy Relationship Schema
```typescript
// Proxy relationship validation
const createProxySchema = z.object({
originalUserID: z.string().min(1),
proxyUserID: z.string().min(1),
startsAt: z.string().datetime(),
expiresAt: z.string().datetime(),
});
// With custom validation
const createProxySchemaWithValidation = createProxySchema.refine(
(data) => new Date(data.expiresAt) > new Date(data.startsAt),
{
message: 'expiresAt must be after startsAt',
path: ['expiresAt'],
}
);
```
### Workflow Validation
```typescript
// Workflow start schema
const startWorkflowSchema = z.object({
workflowCode: z.string().min(1),
entityType: z.enum(['Post', 'User', 'Comment']),
entityID: z.number().int().positive(),
dryRun: z.boolean().optional().default(false),
});
// Workflow step completion schema
const completeStepSchema = z.object({
stepInstanceID: z.number().int().positive(),
answers: z.record(z.string(), z.any()),
dryRun: z.boolean().optional().default(false),
});
```
---
## Route-Level Validation
### Pattern 1: Inline Validation
```typescript
// routes/proxyRoutes.ts
import { z } from 'zod';
const createProxySchema = z.object({
originalUserID: z.string().min(1),
proxyUserID: z.string().min(1),
startsAt: z.string().datetime(),
expiresAt: z.string().datetime(),
});
router.post(
'/',
SSOMiddlewareClient.verifyLoginStatus,
async (req, res) => {
try {
// Validate at route level
const validated = createProxySchema.parse(req.body);
// Delegate to service
const proxy = await proxyService.createProxyRelationship(validated);
res.status(201).json({ success: true, data: proxy });
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({
success: false,
error: {
message: 'Validation failed',
details: error.errors,
},
});
}
handler.handleException(res, error);
}
}
);
```
**Pros:**
- Quick and simple
- Good for simple routes
**Cons:**
- Validation logic in routes
- Harder to test
- Not reusable
---
## Controller Validation
### Pattern 2: Controller Validation (Recommended)
```typescript
// validators/userSchemas.ts
import { z } from 'zod';
export const createUserSchema = z.object({
email: z.string().email(),
name: z.string().min(2).max(100),
roles: z.array(z.enum(['admin', 'operations', 'user'])),
isActive: z.boolean().default(true),
});
export const updateUserSchema = z.object({
email: z.string().email().optional(),
name: z.string().min(2).max(100).optional(),
roles: z.array(z.enum(['admin', 'operations', 'user'])).optional(),
isActive: z.boolean().optional(),
});
export type CreateUserDTO = z.infer<typeof createUserSchema>;
export type UpdateUserDTO = z.infer<typeof updateUserSchema>;
```
```typescript
// controllers/UserController.ts
import { Request, Response } from 'express';
import { BaseController } from './BaseController';
import { UserService } from '../services/userService';
import { createUserSchema, updateUserSchema } from '../validators/userSchemas';
import { z } from 'zod';
export class UserController extends BaseController {
private userService: UserService;
constructor() {
super();
this.userService = new UserService();
}
async createUser(req: Request, res: Response): Promise<void> {
try {
// Validate input
const validated = createUserSchema.parse(req.body);
// Call service
const user = await this.userService.createUser(validated);
this.handleSuccess(res, user, 'User created successfully', 201);
} catch (error) {
if (error instanceof z.ZodError) {
// Handle validation errors with 400 status
return this.handleError(error, res, 'createUser', 400);
}
this.handleError(error, res, 'createUser');
}
}
async updateUser(req: Request, res: Response): Promise<void> {
try {
// Validate params and body
const userId = req.params.id;
const validated = updateUserSchema.parse(req.body);
const user = await this.userService.updateUser(userId, validated);
this.handleSuccess(res, user, 'User updated successfully');
} catch (error) {
if (error instanceof z.ZodError) {
return this.handleError(error, res, 'updateUser', 400);
}
this.handleError(error, res, 'updateUser');
}
}
}
```
**Pros:**
- Clean separation
- Reusable schemas
- Easy to test
- Type-safe DTOs
**Cons:**
- More files to manage
---
## DTO Pattern
### Type Inference from Schemas
```typescript
import { z } from 'zod';
// Define schema
const createUserSchema = z.object({
email: z.string().email(),
name: z.string(),
age: z.number().int().positive(),
});
// Infer TypeScript type from schema
type CreateUserDTO = z.infer<typeof createUserSchema>;
// Equivalent to:
// type CreateUserDTO = {
// email: string;
// name: string;
// age: number;
// }
// Use in service
class UserService {
async createUser(data: CreateUserDTO): Promise<User> {
// data is fully typed!
console.log(data.email); // ✅ TypeScript knows this exists
console.log(data.invalid); // ❌ TypeScript error!
}
}
```
### Input vs Output Types
```typescript
// Input schema (what API receives)
const createUserInputSchema = z.object({
email: z.string().email(),
name: z.string(),
password: z.string().min(8),
});
// Output schema (what API returns)
const userOutputSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
name: z.string(),
createdAt: z.string().datetime(),
// password excluded!
});
type CreateUserInput = z.infer<typeof createUserInputSchema>;
type UserOutput = z.infer<typeof userOutputSchema>;
```
---
## Error Handling
### Zod Error Format
```typescript
try {
const validated = schema.parse(data);
} catch (error) {
if (error instanceof z.ZodError) {
console.log(error.errors);
// [
// {
// code: 'invalid_type',
// expected: 'string',
// received: 'number',
// path: ['email'],
// message: 'Expected string, received number'
// }
// ]
}
}
```
### Custom Error Messages
```typescript
const userSchema = z.object({
email: z.string().email({ message: 'Please provide a valid email address' }),
name: z.string().min(2, { message: 'Name must be at least 2 characters' }),
age: z.number().int().positive({ message: 'Age must be a positive number' }),
});
```
### Formatted Error Response
```typescript
// Helper function to format Zod errors
function formatZodError(error: z.ZodError) {
return {
message: 'Validation failed',
errors: error.errors.map((err) => ({
field: err.path.join('.'),
message: err.message,
code: err.code,
})),
};
}
// In controller
catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({
success: false,
error: formatZodError(error),
});
}
}
// Response example:
// {
// "success": false,
// "error": {
// "message": "Validation failed",
// "errors": [
// {
// "field": "email",
// "message": "Invalid email",
// "code": "invalid_string"
// }
// ]
// }
// }
```
---
## Advanced Patterns
### Conditional Validation
```typescript
// Validate based on other field values
const submissionSchema = z.object({
type: z.enum(['NEW', 'UPDATE']),
postId: z.number().optional(),
}).refine(
(data) => {
// If type is UPDATE, postId is required
if (data.type === 'UPDATE') {
return data.postId !== undefined;
}
return true;
},
{
message: 'postId is required when type is UPDATE',
path: ['postId'],
}
);
```
### Transform Data
```typescript
// Transform strings to numbers
const userSchema = z.object({
name: z.string(),
age: z.string().transform((val) => parseInt(val, 10)),
});
// Transform dates
const eventSchema = z.object({
name: z.string(),
date: z.string().transform((str) => new Date(str)),
});
```
### Preprocess Data
```typescript
// Trim strings before validation
const userSchema = z.object({
email: z.preprocess(
(val) => typeof val === 'string' ? val.trim().toLowerCase() : val,
z.string().email()
),
name: z.preprocess(
(val) => typeof val === 'string' ? val.trim() : val,
z.string().min(2)
),
});
```
### Union Types
```typescript
// Multiple possible types
const idSchema = z.union([z.string(), z.number()]);
// Discriminated unions
const notificationSchema = z.discriminatedUnion('type', [
z.object({
type: z.literal('email'),
recipient: z.string().email(),
subject: z.string(),
}),
z.object({
type: z.literal('sms'),
phoneNumber: z.string(),
message: z.string(),
}),
]);
```
### Recursive Schemas
```typescript
// For nested structures like trees
type Category = {
id: number;
name: string;
children?: Category[];
};
const categorySchema: z.ZodType<Category> = z.lazy(() =>
z.object({
id: z.number(),
name: z.string(),
children: z.array(categorySchema).optional(),
})
);
```
### Schema Composition
```typescript
// Base schemas
const timestampsSchema = z.object({
createdAt: z.string().datetime(),
updatedAt: z.string().datetime(),
});
const auditSchema = z.object({
createdBy: z.string(),
updatedBy: z.string(),
});
// Compose schemas
const userSchema = z.object({
id: z.string(),
email: z.string().email(),
name: z.string(),
}).merge(timestampsSchema).merge(auditSchema);
// Extend schemas
const adminUserSchema = userSchema.extend({
adminLevel: z.number().int().min(1).max(5),
permissions: z.array(z.string()),
});
// Pick specific fields
const publicUserSchema = userSchema.pick({
id: true,
name: true,
// email excluded
});
// Omit fields
const userWithoutTimestamps = userSchema.omit({
createdAt: true,
updatedAt: true,
});
```
### Validation Middleware
```typescript
// Create reusable validation middleware
import { Request, Response, NextFunction } from 'express';
import { z } from 'zod';
export function validateBody<T extends z.ZodType>(schema: T) {
return (req: Request, res: Response, next: NextFunction) => {
try {
req.body = schema.parse(req.body);
next();
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({
success: false,
error: {
message: 'Validation failed',
details: error.errors,
},
});
}
next(error);
}
};
}
// Usage
router.post('/users',
validateBody(createUserSchema),
async (req, res) => {
// req.body is validated and typed!
const user = await userService.createUser(req.body);
res.json({ success: true, data: user });
}
);
```
---
**Related Files:**
- SKILL.md - Main guide
- [routing-and-controllers.md](routing-and-controllers.md) - Using validation in controllers
- [services-and-repositories.md](services-and-repositories.md) - Using DTOs in services
- [async-and-errors.md](async-and-errors.md) - Error handling patterns

View File

@@ -0,0 +1,57 @@
---
name: database-design
description: "Database design principles and decision-making. Schema design, indexing strategy, ORM selection, serverless databases."
risk: unknown
source: community
date_added: "2026-02-27"
---
# Database Design
> **Learn to THINK, not copy SQL patterns.**
## 🎯 Selective Reading Rule
**Read ONLY files relevant to the request!** Check the content map, find what you need.
| File | Description | When to Read |
|------|-------------|--------------|
| `database-selection.md` | PostgreSQL vs Neon vs Turso vs SQLite | Choosing database |
| `orm-selection.md` | Drizzle vs Prisma vs Kysely | Choosing ORM |
| `schema-design.md` | Normalization, PKs, relationships | Designing schema |
| `indexing.md` | Index types, composite indexes | Performance tuning |
| `optimization.md` | N+1, EXPLAIN ANALYZE | Query optimization |
| `migrations.md` | Safe migrations, serverless DBs | Schema changes |
---
## ⚠️ Core Principle
- ASK user for database preferences when unclear
- Choose database/ORM based on CONTEXT
- Don't default to PostgreSQL for everything
---
## Decision Checklist
Before designing schema:
- [ ] Asked user about database preference?
- [ ] Chosen database for THIS context?
- [ ] Considered deployment environment?
- [ ] Planned index strategy?
- [ ] Defined relationship types?
---
## Anti-Patterns
❌ Default to PostgreSQL for simple apps (SQLite may suffice)
❌ Skip indexing
❌ Use SELECT * in production
❌ Store JSON when structured data is better
❌ Ignore N+1 queries
## When to Use
This skill is applicable to execute the workflow or actions described in the overview.

View File

@@ -0,0 +1,43 @@
# Database Selection (2025)
> Choose database based on context, not default.
## Decision Tree
```
What are your requirements?
├── Full relational features needed
│ ├── Self-hosted → PostgreSQL
│ └── Serverless → Neon, Supabase
├── Edge deployment / Ultra-low latency
│ └── Turso (edge SQLite)
├── AI / Vector search
│ └── PostgreSQL + pgvector
├── Simple / Embedded / Local
│ └── SQLite
└── Global distribution
└── PlanetScale, CockroachDB, Turso
```
## Comparison
| Database | Best For | Trade-offs |
|----------|----------|------------|
| **PostgreSQL** | Full features, complex queries | Needs hosting |
| **Neon** | Serverless PG, branching | PG complexity |
| **Turso** | Edge, low latency | SQLite limitations |
| **SQLite** | Simple, embedded, local | Single-writer |
| **PlanetScale** | MySQL, global scale | No foreign keys |
## Questions to Ask
1. What's the deployment environment?
2. How complex are the queries?
3. Is edge/serverless important?
4. Vector search needed?
5. Global distribution required?

View File

@@ -0,0 +1,39 @@
# Indexing Principles
> When and how to create indexes effectively.
## When to Create Indexes
```
Index these:
├── Columns in WHERE clauses
├── Columns in JOIN conditions
├── Columns in ORDER BY
├── Foreign key columns
└── Unique constraints
Don't over-index:
├── Write-heavy tables (slower inserts)
├── Low-cardinality columns
├── Columns rarely queried
```
## Index Type Selection
| Type | Use For |
|------|---------|
| **B-tree** | General purpose, equality & range |
| **Hash** | Equality only, faster |
| **GIN** | JSONB, arrays, full-text |
| **GiST** | Geometric, range types |
| **HNSW/IVFFlat** | Vector similarity (pgvector) |
## Composite Index Principles
```
Order matters for composite indexes:
├── Equality columns first
├── Range columns last
├── Most selective first
└── Match query pattern
```

View File

@@ -0,0 +1,48 @@
# Migration Principles
> Safe migration strategy for zero-downtime changes.
## Safe Migration Strategy
```
For zero-downtime changes:
├── Adding column
│ └── Add as nullable → backfill → add NOT NULL
├── Removing column
│ └── Stop using → deploy → remove column
├── Adding index
│ └── CREATE INDEX CONCURRENTLY (non-blocking)
└── Renaming column
└── Add new → migrate data → deploy → drop old
```
## Migration Philosophy
- Never make breaking changes in one step
- Test migrations on data copy first
- Have rollback plan
- Run in transaction when possible
## Serverless Databases
### Neon (Serverless PostgreSQL)
| Feature | Benefit |
|---------|---------|
| Scale to zero | Cost savings |
| Instant branching | Dev/preview |
| Full PostgreSQL | Compatibility |
| Autoscaling | Traffic handling |
### Turso (Edge SQLite)
| Feature | Benefit |
|---------|---------|
| Edge locations | Ultra-low latency |
| SQLite compatible | Simple |
| Generous free tier | Cost |
| Global distribution | Performance |

View File

@@ -0,0 +1,36 @@
# Query Optimization
> N+1 problem, EXPLAIN ANALYZE, optimization priorities.
## N+1 Problem
```
What is N+1?
├── 1 query to get parent records
├── N queries to get related records
└── Very slow!
Solutions:
├── JOIN → Single query with all data
├── Eager loading → ORM handles JOIN
├── DataLoader → Batch and cache (GraphQL)
└── Subquery → Fetch related in one query
```
## Query Analysis Mindset
```
Before optimizing:
├── EXPLAIN ANALYZE the query
├── Look for Seq Scan (full table scan)
├── Check actual vs estimated rows
└── Identify missing indexes
```
## Optimization Priorities
1. **Add missing indexes** (most common issue)
2. **Select only needed columns** (not SELECT *)
3. **Use proper JOINs** (avoid subqueries when possible)
4. **Limit early** (pagination at database level)
5. **Cache** (when appropriate)

View File

@@ -0,0 +1,30 @@
# ORM Selection (2025)
> Choose ORM based on deployment and DX needs.
## Decision Tree
```
What's the context?
├── Edge deployment / Bundle size matters
│ └── Drizzle (smallest, SQL-like)
├── Best DX / Schema-first
│ └── Prisma (migrations, studio)
├── Maximum control
│ └── Raw SQL with query builder
└── Python ecosystem
└── SQLAlchemy 2.0 (async support)
```
## Comparison
| ORM | Best For | Trade-offs |
|-----|----------|------------|
| **Drizzle** | Edge, TypeScript | Newer, less examples |
| **Prisma** | DX, schema management | Heavier, not edge-ready |
| **Kysely** | Type-safe SQL builder | Manual migrations |
| **Raw SQL** | Complex queries, control | Manual type safety |

View File

@@ -0,0 +1,56 @@
# Schema Design Principles
> Normalization, primary keys, timestamps, relationships.
## Normalization Decision
```
When to normalize (separate tables):
├── Data is repeated across rows
├── Updates would need multiple changes
├── Relationships are clear
└── Query patterns benefit
When to denormalize (embed/duplicate):
├── Read performance critical
├── Data rarely changes
├── Always fetched together
└── Simpler queries needed
```
## Primary Key Selection
| Type | Use When |
|------|----------|
| **UUID** | Distributed systems, security |
| **ULID** | UUID + sortable by time |
| **Auto-increment** | Simple apps, single database |
| **Natural key** | Rarely (business meaning) |
## Timestamp Strategy
```
For every table:
├── created_at → When created
├── updated_at → Last modified
└── deleted_at → Soft delete (if needed)
Use TIMESTAMPTZ (with timezone) not TIMESTAMP
```
## Relationship Types
| Type | When | Implementation |
|------|------|----------------|
| **One-to-One** | Extension data | Separate table with FK |
| **One-to-Many** | Parent-children | FK on child table |
| **Many-to-Many** | Both sides have many | Junction table |
## Foreign Key ON DELETE
```
├── CASCADE → Delete children with parent
├── SET NULL → Children become orphans
├── RESTRICT → Prevent delete if children exist
└── SET DEFAULT → Children get default value
```

View File

@@ -0,0 +1,172 @@
#!/usr/bin/env python3
"""
Schema Validator - Database schema validation
Validates Prisma schemas and checks for common issues.
Usage:
python schema_validator.py <project_path>
Checks:
- Prisma schema syntax
- Missing relations
- Index recommendations
- Naming conventions
"""
import sys
import json
import re
from pathlib import Path
from datetime import datetime
# Fix Windows console encoding
try:
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
except:
pass
def find_schema_files(project_path: Path) -> list:
"""Find database schema files."""
schemas = []
# Prisma schema
prisma_files = list(project_path.glob('**/prisma/schema.prisma'))
schemas.extend([('prisma', f) for f in prisma_files])
# Drizzle schema files
drizzle_files = list(project_path.glob('**/drizzle/*.ts'))
drizzle_files.extend(project_path.glob('**/schema/*.ts'))
for f in drizzle_files:
if 'schema' in f.name.lower() or 'table' in f.name.lower():
schemas.append(('drizzle', f))
return schemas[:10] # Limit
def validate_prisma_schema(file_path: Path) -> list:
"""Validate Prisma schema file."""
issues = []
try:
content = file_path.read_text(encoding='utf-8', errors='ignore')
# Find all models
models = re.findall(r'model\s+(\w+)\s*{([^}]+)}', content, re.DOTALL)
for model_name, model_body in models:
# Check naming convention (PascalCase)
if not model_name[0].isupper():
issues.append(f"Model '{model_name}' should be PascalCase")
# Check for id field
if '@id' not in model_body and 'id' not in model_body.lower():
issues.append(f"Model '{model_name}' might be missing @id field")
# Check for createdAt/updatedAt
if 'createdAt' not in model_body and 'created_at' not in model_body:
issues.append(f"Model '{model_name}' missing createdAt field (recommended)")
# Check for @relation without fields
relations = re.findall(r'@relation\([^)]*\)', model_body)
for rel in relations:
if 'fields:' not in rel and 'references:' not in rel:
pass # Implicit relation, ok
# Check for @@index suggestions
foreign_keys = re.findall(r'(\w+Id)\s+\w+', model_body)
for fk in foreign_keys:
if f'@@index([{fk}])' not in content and f'@@index(["{fk}"])' not in content:
issues.append(f"Consider adding @@index([{fk}]) for better query performance in {model_name}")
# Check for enum definitions
enums = re.findall(r'enum\s+(\w+)\s*{', content)
for enum_name in enums:
if not enum_name[0].isupper():
issues.append(f"Enum '{enum_name}' should be PascalCase")
except Exception as e:
issues.append(f"Error reading schema: {str(e)[:50]}")
return issues
def main():
project_path = Path(sys.argv[1] if len(sys.argv) > 1 else ".").resolve()
print(f"\n{'='*60}")
print(f"[SCHEMA VALIDATOR] Database Schema Validation")
print(f"{'='*60}")
print(f"Project: {project_path}")
print(f"Time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print("-"*60)
# Find schema files
schemas = find_schema_files(project_path)
print(f"Found {len(schemas)} schema files")
if not schemas:
output = {
"script": "schema_validator",
"project": str(project_path),
"schemas_checked": 0,
"issues_found": 0,
"passed": True,
"message": "No schema files found"
}
print(json.dumps(output, indent=2))
sys.exit(0)
# Validate each schema
all_issues = []
for schema_type, file_path in schemas:
print(f"\nValidating: {file_path.name} ({schema_type})")
if schema_type == 'prisma':
issues = validate_prisma_schema(file_path)
else:
issues = [] # Drizzle validation could be added
if issues:
all_issues.append({
"file": str(file_path.name),
"type": schema_type,
"issues": issues
})
# Summary
print("\n" + "="*60)
print("SCHEMA ISSUES")
print("="*60)
if all_issues:
for item in all_issues:
print(f"\n{item['file']} ({item['type']}):")
for issue in item["issues"][:5]: # Limit per file
print(f" - {issue}")
if len(item["issues"]) > 5:
print(f" ... and {len(item['issues']) - 5} more issues")
else:
print("No schema issues found!")
total_issues = sum(len(item["issues"]) for item in all_issues)
# Schema issues are warnings, not failures
passed = True
output = {
"script": "schema_validator",
"project": str(project_path),
"schemas_checked": len(schemas),
"issues_found": total_issues,
"passed": passed,
"issues": all_issues
}
print("\n" + json.dumps(output, indent=2))
sys.exit(0)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,169 @@
---
name: frontend-developer
description: Build React components, implement responsive layouts, and handle client-side state management. Masters React 19, Next.js 15, and modern frontend architecture.
risk: unknown
source: community
date_added: '2026-02-27'
---
You are a frontend development expert specializing in modern React applications, Next.js, and cutting-edge frontend architecture.
## Use this skill when
- Building React or Next.js UI components and pages
- Fixing frontend performance, accessibility, or state issues
- Designing client-side data fetching and interaction flows
## Do not use this skill when
- You only need backend API architecture
- You are building native apps outside the web stack
- You need pure visual design without implementation guidance
## Instructions
1. Clarify requirements, target devices, and performance goals.
2. Choose component structure and state or data approach.
3. Implement UI with accessibility and responsive behavior.
4. Validate performance and UX with profiling and audits.
## Purpose
Expert frontend developer specializing in React 19+, Next.js 15+, and modern web application development. Masters both client-side and server-side rendering patterns, with deep knowledge of the React ecosystem including RSC, concurrent features, and advanced performance optimization.
## Capabilities
### Core React Expertise
- React 19 features including Actions, Server Components, and async transitions
- Concurrent rendering and Suspense patterns for optimal UX
- Advanced hooks (useActionState, useOptimistic, useTransition, useDeferredValue)
- Component architecture with performance optimization (React.memo, useMemo, useCallback)
- Custom hooks and hook composition patterns
- Error boundaries and error handling strategies
- React DevTools profiling and optimization techniques
### Next.js & Full-Stack Integration
- Next.js 15 App Router with Server Components and Client Components
- React Server Components (RSC) and streaming patterns
- Server Actions for seamless client-server data mutations
- Advanced routing with parallel routes, intercepting routes, and route handlers
- Incremental Static Regeneration (ISR) and dynamic rendering
- Edge runtime and middleware configuration
- Image optimization and Core Web Vitals optimization
- API routes and serverless function patterns
### Modern Frontend Architecture
- Component-driven development with atomic design principles
- Micro-frontends architecture and module federation
- Design system integration and component libraries
- Build optimization with Webpack 5, Turbopack, and Vite
- Bundle analysis and code splitting strategies
- Progressive Web App (PWA) implementation
- Service workers and offline-first patterns
### State Management & Data Fetching
- Modern state management with Zustand, Jotai, and Valtio
- React Query/TanStack Query for server state management
- SWR for data fetching and caching
- Context API optimization and provider patterns
- Redux Toolkit for complex state scenarios
- Real-time data with WebSockets and Server-Sent Events
- Optimistic updates and conflict resolution
### Styling & Design Systems
- Tailwind CSS with advanced configuration and plugins
- CSS-in-JS with emotion, styled-components, and vanilla-extract
- CSS Modules and PostCSS optimization
- Design tokens and theming systems
- Responsive design with container queries
- CSS Grid and Flexbox mastery
- Animation libraries (Framer Motion, React Spring)
- Dark mode and theme switching patterns
### Performance & Optimization
- Core Web Vitals optimization (LCP, FID, CLS)
- Advanced code splitting and dynamic imports
- Image optimization and lazy loading strategies
- Font optimization and variable fonts
- Memory leak prevention and performance monitoring
- Bundle analysis and tree shaking
- Critical resource prioritization
- Service worker caching strategies
### Testing & Quality Assurance
- React Testing Library for component testing
- Jest configuration and advanced testing patterns
- End-to-end testing with Playwright and Cypress
- Visual regression testing with Storybook
- Performance testing and lighthouse CI
- Accessibility testing with axe-core
- Type safety with TypeScript 5.x features
### Accessibility & Inclusive Design
- WCAG 2.1/2.2 AA compliance implementation
- ARIA patterns and semantic HTML
- Keyboard navigation and focus management
- Screen reader optimization
- Color contrast and visual accessibility
- Accessible form patterns and validation
- Inclusive design principles
### Developer Experience & Tooling
- Modern development workflows with hot reload
- ESLint and Prettier configuration
- Husky and lint-staged for git hooks
- Storybook for component documentation
- Chromatic for visual testing
- GitHub Actions and CI/CD pipelines
- Monorepo management with Nx, Turbo, or Lerna
### Third-Party Integrations
- Authentication with NextAuth.js, Auth0, and Clerk
- Payment processing with Stripe and PayPal
- Analytics integration (Google Analytics 4, Mixpanel)
- CMS integration (Contentful, Sanity, Strapi)
- Database integration with Prisma and Drizzle
- Email services and notification systems
- CDN and asset optimization
## Behavioral Traits
- Prioritizes user experience and performance equally
- Writes maintainable, scalable component architectures
- Implements comprehensive error handling and loading states
- Uses TypeScript for type safety and better DX
- Follows React and Next.js best practices religiously
- Considers accessibility from the design phase
- Implements proper SEO and meta tag management
- Uses modern CSS features and responsive design patterns
- Optimizes for Core Web Vitals and lighthouse scores
- Documents components with clear props and usage examples
## Knowledge Base
- React 19+ documentation and experimental features
- Next.js 15+ App Router patterns and best practices
- TypeScript 5.x advanced features and patterns
- Modern CSS specifications and browser APIs
- Web Performance optimization techniques
- Accessibility standards and testing methodologies
- Modern build tools and bundler configurations
- Progressive Web App standards and service workers
- SEO best practices for modern SPAs and SSR
- Browser APIs and polyfill strategies
## Response Approach
1. **Analyze requirements** for modern React/Next.js patterns
2. **Suggest performance-optimized solutions** using React 19 features
3. **Provide production-ready code** with proper TypeScript types
4. **Include accessibility considerations** and ARIA patterns
5. **Consider SEO and meta tag implications** for SSR/SSG
6. **Implement proper error boundaries** and loading states
7. **Optimize for Core Web Vitals** and user experience
8. **Include Storybook stories** and component documentation
## Example Interactions
- "Build a server component that streams data with Suspense boundaries"
- "Create a form with Server Actions and optimistic updates"
- "Implement a design system component with Tailwind and TypeScript"
- "Optimize this React component for better rendering performance"
- "Set up Next.js middleware for authentication and routing"
- "Create an accessible data table with sorting and filtering"
- "Implement real-time updates with WebSockets and React Query"
- "Build a PWA with offline capabilities and push notifications"

View File

@@ -0,0 +1,215 @@
---
name: senior-fullstack
description: "Complete toolkit for senior fullstack with modern tools and best practices."
risk: unknown
source: community
date_added: "2026-02-27"
---
# Senior Fullstack
Complete toolkit for senior fullstack with modern tools and best practices.
## Quick Start
### Main Capabilities
This skill provides three core capabilities through automated scripts:
```bash
# Script 1: Fullstack Scaffolder
python scripts/fullstack_scaffolder.py [options]
# Script 2: Project Scaffolder
python scripts/project_scaffolder.py [options]
# Script 3: Code Quality Analyzer
python scripts/code_quality_analyzer.py [options]
```
## Core Capabilities
### 1. Fullstack Scaffolder
Automated tool for fullstack scaffolder tasks.
**Features:**
- Automated scaffolding
- Best practices built-in
- Configurable templates
- Quality checks
**Usage:**
```bash
python scripts/fullstack_scaffolder.py <project-path> [options]
```
### 2. Project Scaffolder
Comprehensive analysis and optimization tool.
**Features:**
- Deep analysis
- Performance metrics
- Recommendations
- Automated fixes
**Usage:**
```bash
python scripts/project_scaffolder.py <target-path> [--verbose]
```
### 3. Code Quality Analyzer
Advanced tooling for specialized tasks.
**Features:**
- Expert-level automation
- Custom configurations
- Integration ready
- Production-grade output
**Usage:**
```bash
python scripts/code_quality_analyzer.py [arguments] [options]
```
## Reference Documentation
### Tech Stack Guide
Comprehensive guide available in `references/tech_stack_guide.md`:
- Detailed patterns and practices
- Code examples
- Best practices
- Anti-patterns to avoid
- Real-world scenarios
### Architecture Patterns
Complete workflow documentation in `references/architecture_patterns.md`:
- Step-by-step processes
- Optimization strategies
- Tool integrations
- Performance tuning
- Troubleshooting guide
### Development Workflows
Technical reference guide in `references/development_workflows.md`:
- Technology stack details
- Configuration examples
- Integration patterns
- Security considerations
- Scalability guidelines
## Tech Stack
**Languages:** TypeScript, JavaScript, Python, Go, Swift, Kotlin
**Frontend:** React, Next.js, React Native, Flutter
**Backend:** Node.js, Express, GraphQL, REST APIs
**Database:** PostgreSQL, Prisma, NeonDB, Supabase
**DevOps:** Docker, Kubernetes, Terraform, GitHub Actions, CircleCI
**Cloud:** AWS, GCP, Azure
## Development Workflow
### 1. Setup and Configuration
```bash
# Install dependencies
npm install
# or
pip install -r requirements.txt
# Configure environment
cp .env.example .env
```
### 2. Run Quality Checks
```bash
# Use the analyzer script
python scripts/project_scaffolder.py .
# Review recommendations
# Apply fixes
```
### 3. Implement Best Practices
Follow the patterns and practices documented in:
- `references/tech_stack_guide.md`
- `references/architecture_patterns.md`
- `references/development_workflows.md`
## Best Practices Summary
### Code Quality
- Follow established patterns
- Write comprehensive tests
- Document decisions
- Review regularly
### Performance
- Measure before optimizing
- Use appropriate caching
- Optimize critical paths
- Monitor in production
### Security
- Validate all inputs
- Use parameterized queries
- Implement proper authentication
- Keep dependencies updated
### Maintainability
- Write clear code
- Use consistent naming
- Add helpful comments
- Keep it simple
## Common Commands
```bash
# Development
npm run dev
npm run build
npm run test
npm run lint
# Analysis
python scripts/project_scaffolder.py .
python scripts/code_quality_analyzer.py --analyze
# Deployment
docker build -t app:latest .
docker-compose up -d
kubectl apply -f k8s/
```
## Troubleshooting
### Common Issues
Check the comprehensive troubleshooting section in `references/development_workflows.md`.
### Getting Help
- Review reference documentation
- Check script output messages
- Consult tech stack documentation
- Review error logs
## Resources
- Pattern Reference: `references/tech_stack_guide.md`
- Workflow Guide: `references/architecture_patterns.md`
- Technical Guide: `references/development_workflows.md`
- Tool Scripts: `scripts/` directory
## When to Use
This skill is applicable to execute the workflow or actions described in the overview.

View File

@@ -0,0 +1,103 @@
# Architecture Patterns
## Overview
This reference guide provides comprehensive information for senior fullstack.
## Patterns and Practices
### Pattern 1: Best Practice Implementation
**Description:**
Detailed explanation of the pattern.
**When to Use:**
- Scenario 1
- Scenario 2
- Scenario 3
**Implementation:**
```typescript
// Example code implementation
export class Example {
// Implementation details
}
```
**Benefits:**
- Benefit 1
- Benefit 2
- Benefit 3
**Trade-offs:**
- Consider 1
- Consider 2
- Consider 3
### Pattern 2: Advanced Technique
**Description:**
Another important pattern for senior fullstack.
**Implementation:**
```typescript
// Advanced example
async function advancedExample() {
// Code here
}
```
## Guidelines
### Code Organization
- Clear structure
- Logical separation
- Consistent naming
- Proper documentation
### Performance Considerations
- Optimization strategies
- Bottleneck identification
- Monitoring approaches
- Scaling techniques
### Security Best Practices
- Input validation
- Authentication
- Authorization
- Data protection
## Common Patterns
### Pattern A
Implementation details and examples.
### Pattern B
Implementation details and examples.
### Pattern C
Implementation details and examples.
## Anti-Patterns to Avoid
### Anti-Pattern 1
What not to do and why.
### Anti-Pattern 2
What not to do and why.
## Tools and Resources
### Recommended Tools
- Tool 1: Purpose
- Tool 2: Purpose
- Tool 3: Purpose
### Further Reading
- Resource 1
- Resource 2
- Resource 3
## Conclusion
Key takeaways for using this reference guide effectively.

View File

@@ -0,0 +1,103 @@
# Development Workflows
## Overview
This reference guide provides comprehensive information for senior fullstack.
## Patterns and Practices
### Pattern 1: Best Practice Implementation
**Description:**
Detailed explanation of the pattern.
**When to Use:**
- Scenario 1
- Scenario 2
- Scenario 3
**Implementation:**
```typescript
// Example code implementation
export class Example {
// Implementation details
}
```
**Benefits:**
- Benefit 1
- Benefit 2
- Benefit 3
**Trade-offs:**
- Consider 1
- Consider 2
- Consider 3
### Pattern 2: Advanced Technique
**Description:**
Another important pattern for senior fullstack.
**Implementation:**
```typescript
// Advanced example
async function advancedExample() {
// Code here
}
```
## Guidelines
### Code Organization
- Clear structure
- Logical separation
- Consistent naming
- Proper documentation
### Performance Considerations
- Optimization strategies
- Bottleneck identification
- Monitoring approaches
- Scaling techniques
### Security Best Practices
- Input validation
- Authentication
- Authorization
- Data protection
## Common Patterns
### Pattern A
Implementation details and examples.
### Pattern B
Implementation details and examples.
### Pattern C
Implementation details and examples.
## Anti-Patterns to Avoid
### Anti-Pattern 1
What not to do and why.
### Anti-Pattern 2
What not to do and why.
## Tools and Resources
### Recommended Tools
- Tool 1: Purpose
- Tool 2: Purpose
- Tool 3: Purpose
### Further Reading
- Resource 1
- Resource 2
- Resource 3
## Conclusion
Key takeaways for using this reference guide effectively.

View File

@@ -0,0 +1,103 @@
# Tech Stack Guide
## Overview
This reference guide provides comprehensive information for senior fullstack.
## Patterns and Practices
### Pattern 1: Best Practice Implementation
**Description:**
Detailed explanation of the pattern.
**When to Use:**
- Scenario 1
- Scenario 2
- Scenario 3
**Implementation:**
```typescript
// Example code implementation
export class Example {
// Implementation details
}
```
**Benefits:**
- Benefit 1
- Benefit 2
- Benefit 3
**Trade-offs:**
- Consider 1
- Consider 2
- Consider 3
### Pattern 2: Advanced Technique
**Description:**
Another important pattern for senior fullstack.
**Implementation:**
```typescript
// Advanced example
async function advancedExample() {
// Code here
}
```
## Guidelines
### Code Organization
- Clear structure
- Logical separation
- Consistent naming
- Proper documentation
### Performance Considerations
- Optimization strategies
- Bottleneck identification
- Monitoring approaches
- Scaling techniques
### Security Best Practices
- Input validation
- Authentication
- Authorization
- Data protection
## Common Patterns
### Pattern A
Implementation details and examples.
### Pattern B
Implementation details and examples.
### Pattern C
Implementation details and examples.
## Anti-Patterns to Avoid
### Anti-Pattern 1
What not to do and why.
### Anti-Pattern 2
What not to do and why.
## Tools and Resources
### Recommended Tools
- Tool 1: Purpose
- Tool 2: Purpose
- Tool 3: Purpose
### Further Reading
- Resource 1
- Resource 2
- Resource 3
## Conclusion
Key takeaways for using this reference guide effectively.

View File

@@ -0,0 +1,114 @@
#!/usr/bin/env python3
"""
Code Quality Analyzer
Automated tool for senior fullstack tasks
"""
import os
import sys
import json
import argparse
from pathlib import Path
from typing import Dict, List, Optional
class CodeQualityAnalyzer:
"""Main class for code quality analyzer functionality"""
def __init__(self, target_path: str, verbose: bool = False):
self.target_path = Path(target_path)
self.verbose = verbose
self.results = {}
def run(self) -> Dict:
"""Execute the main functionality"""
print(f"🚀 Running {self.__class__.__name__}...")
print(f"📁 Target: {self.target_path}")
try:
self.validate_target()
self.analyze()
self.generate_report()
print("✅ Completed successfully!")
return self.results
except Exception as e:
print(f"❌ Error: {e}")
sys.exit(1)
def validate_target(self):
"""Validate the target path exists and is accessible"""
if not self.target_path.exists():
raise ValueError(f"Target path does not exist: {self.target_path}")
if self.verbose:
print(f"✓ Target validated: {self.target_path}")
def analyze(self):
"""Perform the main analysis or operation"""
if self.verbose:
print("📊 Analyzing...")
# Main logic here
self.results['status'] = 'success'
self.results['target'] = str(self.target_path)
self.results['findings'] = []
# Add analysis results
if self.verbose:
print(f"✓ Analysis complete: {len(self.results.get('findings', []))} findings")
def generate_report(self):
"""Generate and display the report"""
print("\n" + "="*50)
print("REPORT")
print("="*50)
print(f"Target: {self.results.get('target')}")
print(f"Status: {self.results.get('status')}")
print(f"Findings: {len(self.results.get('findings', []))}")
print("="*50 + "\n")
def main():
"""Main entry point"""
parser = argparse.ArgumentParser(
description="Code Quality Analyzer"
)
parser.add_argument(
'target',
help='Target path to analyze or process'
)
parser.add_argument(
'--verbose', '-v',
action='store_true',
help='Enable verbose output'
)
parser.add_argument(
'--json',
action='store_true',
help='Output results as JSON'
)
parser.add_argument(
'--output', '-o',
help='Output file path'
)
args = parser.parse_args()
tool = CodeQualityAnalyzer(
args.target,
verbose=args.verbose
)
results = tool.run()
if args.json:
output = json.dumps(results, indent=2)
if args.output:
with open(args.output, 'w') as f:
f.write(output)
print(f"Results written to {args.output}")
else:
print(output)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,114 @@
#!/usr/bin/env python3
"""
Fullstack Scaffolder
Automated tool for senior fullstack tasks
"""
import os
import sys
import json
import argparse
from pathlib import Path
from typing import Dict, List, Optional
class FullstackScaffolder:
"""Main class for fullstack scaffolder functionality"""
def __init__(self, target_path: str, verbose: bool = False):
self.target_path = Path(target_path)
self.verbose = verbose
self.results = {}
def run(self) -> Dict:
"""Execute the main functionality"""
print(f"🚀 Running {self.__class__.__name__}...")
print(f"📁 Target: {self.target_path}")
try:
self.validate_target()
self.analyze()
self.generate_report()
print("✅ Completed successfully!")
return self.results
except Exception as e:
print(f"❌ Error: {e}")
sys.exit(1)
def validate_target(self):
"""Validate the target path exists and is accessible"""
if not self.target_path.exists():
raise ValueError(f"Target path does not exist: {self.target_path}")
if self.verbose:
print(f"✓ Target validated: {self.target_path}")
def analyze(self):
"""Perform the main analysis or operation"""
if self.verbose:
print("📊 Analyzing...")
# Main logic here
self.results['status'] = 'success'
self.results['target'] = str(self.target_path)
self.results['findings'] = []
# Add analysis results
if self.verbose:
print(f"✓ Analysis complete: {len(self.results.get('findings', []))} findings")
def generate_report(self):
"""Generate and display the report"""
print("\n" + "="*50)
print("REPORT")
print("="*50)
print(f"Target: {self.results.get('target')}")
print(f"Status: {self.results.get('status')}")
print(f"Findings: {len(self.results.get('findings', []))}")
print("="*50 + "\n")
def main():
"""Main entry point"""
parser = argparse.ArgumentParser(
description="Fullstack Scaffolder"
)
parser.add_argument(
'target',
help='Target path to analyze or process'
)
parser.add_argument(
'--verbose', '-v',
action='store_true',
help='Enable verbose output'
)
parser.add_argument(
'--json',
action='store_true',
help='Output results as JSON'
)
parser.add_argument(
'--output', '-o',
help='Output file path'
)
args = parser.parse_args()
tool = FullstackScaffolder(
args.target,
verbose=args.verbose
)
results = tool.run()
if args.json:
output = json.dumps(results, indent=2)
if args.output:
with open(args.output, 'w') as f:
f.write(output)
print(f"Results written to {args.output}")
else:
print(output)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,114 @@
#!/usr/bin/env python3
"""
Project Scaffolder
Automated tool for senior fullstack tasks
"""
import os
import sys
import json
import argparse
from pathlib import Path
from typing import Dict, List, Optional
class ProjectScaffolder:
"""Main class for project scaffolder functionality"""
def __init__(self, target_path: str, verbose: bool = False):
self.target_path = Path(target_path)
self.verbose = verbose
self.results = {}
def run(self) -> Dict:
"""Execute the main functionality"""
print(f"🚀 Running {self.__class__.__name__}...")
print(f"📁 Target: {self.target_path}")
try:
self.validate_target()
self.analyze()
self.generate_report()
print("✅ Completed successfully!")
return self.results
except Exception as e:
print(f"❌ Error: {e}")
sys.exit(1)
def validate_target(self):
"""Validate the target path exists and is accessible"""
if not self.target_path.exists():
raise ValueError(f"Target path does not exist: {self.target_path}")
if self.verbose:
print(f"✓ Target validated: {self.target_path}")
def analyze(self):
"""Perform the main analysis or operation"""
if self.verbose:
print("📊 Analyzing...")
# Main logic here
self.results['status'] = 'success'
self.results['target'] = str(self.target_path)
self.results['findings'] = []
# Add analysis results
if self.verbose:
print(f"✓ Analysis complete: {len(self.results.get('findings', []))} findings")
def generate_report(self):
"""Generate and display the report"""
print("\n" + "="*50)
print("REPORT")
print("="*50)
print(f"Target: {self.results.get('target')}")
print(f"Status: {self.results.get('status')}")
print(f"Findings: {len(self.results.get('findings', []))}")
print("="*50 + "\n")
def main():
"""Main entry point"""
parser = argparse.ArgumentParser(
description="Project Scaffolder"
)
parser.add_argument(
'target',
help='Target path to analyze or process'
)
parser.add_argument(
'--verbose', '-v',
action='store_true',
help='Enable verbose output'
)
parser.add_argument(
'--json',
action='store_true',
help='Output results as JSON'
)
parser.add_argument(
'--output', '-o',
help='Output file path'
)
args = parser.parse_args()
tool = ProjectScaffolder(
args.target,
verbose=args.verbose
)
results = tool.run()
if args.json:
output = json.dumps(results, indent=2)
if args.output:
with open(args.output, 'w') as f:
f.write(output)
print(f"Results written to {args.output}")
else:
print(output)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,457 @@
---
name: stripe-integration
description: "Master Stripe payment processing integration for robust, PCI-compliant payment flows including checkout, subscriptions, webhooks, and refunds."
risk: unknown
source: community
date_added: "2026-02-27"
---
# Stripe Integration
Master Stripe payment processing integration for robust, PCI-compliant payment flows including checkout, subscriptions, webhooks, and refunds.
## Do not use this skill when
- The task is unrelated to stripe integration
- You need a different domain or tool outside this scope
## Instructions
- Clarify goals, constraints, and required inputs.
- Apply relevant best practices and validate outcomes.
- Provide actionable steps and verification.
- If detailed examples are required, open `resources/implementation-playbook.md`.
## Use this skill when
- Implementing payment processing in web/mobile applications
- Setting up subscription billing systems
- Handling one-time payments and recurring charges
- Processing refunds and disputes
- Managing customer payment methods
- Implementing SCA (Strong Customer Authentication) for European payments
- Building marketplace payment flows with Stripe Connect
## Core Concepts
### 1. Payment Flows
**Checkout Session (Hosted)**
- Stripe-hosted payment page
- Minimal PCI compliance burden
- Fastest implementation
- Supports one-time and recurring payments
**Payment Intents (Custom UI)**
- Full control over payment UI
- Requires Stripe.js for PCI compliance
- More complex implementation
- Better customization options
**Setup Intents (Save Payment Methods)**
- Collect payment method without charging
- Used for subscriptions and future payments
- Requires customer confirmation
### 2. Webhooks
**Critical Events:**
- `payment_intent.succeeded`: Payment completed
- `payment_intent.payment_failed`: Payment failed
- `customer.subscription.updated`: Subscription changed
- `customer.subscription.deleted`: Subscription canceled
- `charge.refunded`: Refund processed
- `invoice.payment_succeeded`: Subscription payment successful
### 3. Subscriptions
**Components:**
- **Product**: What you're selling
- **Price**: How much and how often
- **Subscription**: Customer's recurring payment
- **Invoice**: Generated for each billing cycle
### 4. Customer Management
- Create and manage customer records
- Store multiple payment methods
- Track customer metadata
- Manage billing details
## Quick Start
```python
import stripe
stripe.api_key = "sk_test_..."
# Create a checkout session
session = stripe.checkout.Session.create(
payment_method_types=['card'],
line_items=[{
'price_data': {
'currency': 'usd',
'product_data': {
'name': 'Premium Subscription',
},
'unit_amount': 2000, # $20.00
'recurring': {
'interval': 'month',
},
},
'quantity': 1,
}],
mode='subscription',
success_url='https://yourdomain.com/success?session_id={CHECKOUT_SESSION_ID}',
cancel_url='https://yourdomain.com/cancel',
)
# Redirect user to session.url
print(session.url)
```
## Payment Implementation Patterns
### Pattern 1: One-Time Payment (Hosted Checkout)
```python
def create_checkout_session(amount, currency='usd'):
"""Create a one-time payment checkout session."""
try:
session = stripe.checkout.Session.create(
payment_method_types=['card'],
line_items=[{
'price_data': {
'currency': currency,
'product_data': {
'name': 'Purchase',
'images': ['https://example.com/product.jpg'],
},
'unit_amount': amount, # Amount in cents
},
'quantity': 1,
}],
mode='payment',
success_url='https://yourdomain.com/success?session_id={CHECKOUT_SESSION_ID}',
cancel_url='https://yourdomain.com/cancel',
metadata={
'order_id': 'order_123',
'user_id': 'user_456'
}
)
return session
except stripe.error.StripeError as e:
# Handle error
print(f"Stripe error: {e.user_message}")
raise
```
### Pattern 2: Custom Payment Intent Flow
```python
def create_payment_intent(amount, currency='usd', customer_id=None):
"""Create a payment intent for custom checkout UI."""
intent = stripe.PaymentIntent.create(
amount=amount,
currency=currency,
customer=customer_id,
automatic_payment_methods={
'enabled': True,
},
metadata={
'integration_check': 'accept_a_payment'
}
)
return intent.client_secret # Send to frontend
# Frontend (JavaScript)
"""
const stripe = Stripe('pk_test_...');
const elements = stripe.elements();
const cardElement = elements.create('card');
cardElement.mount('#card-element');
const {error, paymentIntent} = await stripe.confirmCardPayment(
clientSecret,
{
payment_method: {
card: cardElement,
billing_details: {
name: 'Customer Name'
}
}
}
);
if (error) {
// Handle error
} else if (paymentIntent.status === 'succeeded') {
// Payment successful
}
"""
```
### Pattern 3: Subscription Creation
```python
def create_subscription(customer_id, price_id):
"""Create a subscription for a customer."""
try:
subscription = stripe.Subscription.create(
customer=customer_id,
items=[{'price': price_id}],
payment_behavior='default_incomplete',
payment_settings={'save_default_payment_method': 'on_subscription'},
expand=['latest_invoice.payment_intent'],
)
return {
'subscription_id': subscription.id,
'client_secret': subscription.latest_invoice.payment_intent.client_secret
}
except stripe.error.StripeError as e:
print(f"Subscription creation failed: {e}")
raise
```
### Pattern 4: Customer Portal
```python
def create_customer_portal_session(customer_id):
"""Create a portal session for customers to manage subscriptions."""
session = stripe.billing_portal.Session.create(
customer=customer_id,
return_url='https://yourdomain.com/account',
)
return session.url # Redirect customer here
```
## Webhook Handling
### Secure Webhook Endpoint
```python
from flask import Flask, request
import stripe
app = Flask(__name__)
endpoint_secret = 'whsec_...'
@app.route('/webhook', methods=['POST'])
def webhook():
payload = request.data
sig_header = request.headers.get('Stripe-Signature')
try:
event = stripe.Webhook.construct_event(
payload, sig_header, endpoint_secret
)
except ValueError:
# Invalid payload
return 'Invalid payload', 400
except stripe.error.SignatureVerificationError:
# Invalid signature
return 'Invalid signature', 400
# Handle the event
if event['type'] == 'payment_intent.succeeded':
payment_intent = event['data']['object']
handle_successful_payment(payment_intent)
elif event['type'] == 'payment_intent.payment_failed':
payment_intent = event['data']['object']
handle_failed_payment(payment_intent)
elif event['type'] == 'customer.subscription.deleted':
subscription = event['data']['object']
handle_subscription_canceled(subscription)
return 'Success', 200
def handle_successful_payment(payment_intent):
"""Process successful payment."""
customer_id = payment_intent.get('customer')
amount = payment_intent['amount']
metadata = payment_intent.get('metadata', {})
# Update your database
# Send confirmation email
# Fulfill order
print(f"Payment succeeded: {payment_intent['id']}")
def handle_failed_payment(payment_intent):
"""Handle failed payment."""
error = payment_intent.get('last_payment_error', {})
print(f"Payment failed: {error.get('message')}")
# Notify customer
# Update order status
def handle_subscription_canceled(subscription):
"""Handle subscription cancellation."""
customer_id = subscription['customer']
# Update user access
# Send cancellation email
print(f"Subscription canceled: {subscription['id']}")
```
### Webhook Best Practices
```python
import hashlib
import hmac
def verify_webhook_signature(payload, signature, secret):
"""Manually verify webhook signature."""
expected_sig = hmac.new(
secret.encode('utf-8'),
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected_sig)
def handle_webhook_idempotently(event_id, handler):
"""Ensure webhook is processed exactly once."""
# Check if event already processed
if is_event_processed(event_id):
return
# Process event
try:
handler()
mark_event_processed(event_id)
except Exception as e:
log_error(e)
# Stripe will retry failed webhooks
raise
```
## Customer Management
```python
def create_customer(email, name, payment_method_id=None):
"""Create a Stripe customer."""
customer = stripe.Customer.create(
email=email,
name=name,
payment_method=payment_method_id,
invoice_settings={
'default_payment_method': payment_method_id
} if payment_method_id else None,
metadata={
'user_id': '12345'
}
)
return customer
def attach_payment_method(customer_id, payment_method_id):
"""Attach a payment method to a customer."""
stripe.PaymentMethod.attach(
payment_method_id,
customer=customer_id
)
# Set as default
stripe.Customer.modify(
customer_id,
invoice_settings={
'default_payment_method': payment_method_id
}
)
def list_customer_payment_methods(customer_id):
"""List all payment methods for a customer."""
payment_methods = stripe.PaymentMethod.list(
customer=customer_id,
type='card'
)
return payment_methods.data
```
## Refund Handling
```python
def create_refund(payment_intent_id, amount=None, reason=None):
"""Create a refund."""
refund_params = {
'payment_intent': payment_intent_id
}
if amount:
refund_params['amount'] = amount # Partial refund
if reason:
refund_params['reason'] = reason # 'duplicate', 'fraudulent', 'requested_by_customer'
refund = stripe.Refund.create(**refund_params)
return refund
def handle_dispute(charge_id, evidence):
"""Update dispute with evidence."""
stripe.Dispute.modify(
charge_id,
evidence={
'customer_name': evidence.get('customer_name'),
'customer_email_address': evidence.get('customer_email'),
'shipping_documentation': evidence.get('shipping_proof'),
'customer_communication': evidence.get('communication'),
}
)
```
## Testing
```python
# Use test mode keys
stripe.api_key = "sk_test_..."
# Test card numbers
TEST_CARDS = {
'success': '4242424242424242',
'declined': '4000000000000002',
'3d_secure': '4000002500003155',
'insufficient_funds': '4000000000009995'
}
def test_payment_flow():
"""Test complete payment flow."""
# Create test customer
customer = stripe.Customer.create(
email="test@example.com"
)
# Create payment intent
intent = stripe.PaymentIntent.create(
amount=1000,
currency='usd',
customer=customer.id,
payment_method_types=['card']
)
# Confirm with test card
confirmed = stripe.PaymentIntent.confirm(
intent.id,
payment_method='pm_card_visa' # Test payment method
)
assert confirmed.status == 'succeeded'
```
## Resources
- **references/checkout-flows.md**: Detailed checkout implementation
- **references/webhook-handling.md**: Webhook security and processing
- **references/subscription-management.md**: Subscription lifecycle
- **references/customer-management.md**: Customer and payment method handling
- **references/invoice-generation.md**: Invoicing and billing
- **assets/stripe-client.py**: Production-ready Stripe client wrapper
- **assets/webhook-handler.py**: Complete webhook processor
- **assets/checkout-config.json**: Checkout configuration templates
## Best Practices
1. **Always Use Webhooks**: Don't rely solely on client-side confirmation
2. **Idempotency**: Handle webhook events idempotently
3. **Error Handling**: Gracefully handle all Stripe errors
4. **Test Mode**: Thoroughly test with test keys before production
5. **Metadata**: Use metadata to link Stripe objects to your database
6. **Monitoring**: Track payment success rates and errors
7. **PCI Compliance**: Never handle raw card data on your server
8. **SCA Ready**: Implement 3D Secure for European payments
## Common Pitfalls
- **Not Verifying Webhooks**: Always verify webhook signatures
- **Missing Webhook Events**: Handle all relevant webhook events
- **Hardcoded Amounts**: Use cents/smallest currency unit
- **No Retry Logic**: Implement retries for API calls
- **Ignoring Test Mode**: Test all edge cases with test cards