feat(bundles): add editorial bundle plugins
This commit is contained in:
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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?
|
||||
@@ -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
|
||||
```
|
||||
@@ -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
|
||||
```
|
||||
@@ -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
|
||||
```
|
||||
@@ -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
|
||||
```
|
||||
@@ -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?
|
||||
@@ -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 |
|
||||
@@ -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()
|
||||
@@ -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.
|
||||
@@ -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
|
||||
```
|
||||
@@ -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
|
||||
```
|
||||
@@ -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 (1–5)
|
||||
|
||||
| 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 |
|
||||
| -------- | --------- | ---------------------- |
|
||||
| **6–10** | Safe | Proceed |
|
||||
| **3–5** | Moderate | Add tests + monitoring |
|
||||
| **0–2** | 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.
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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?
|
||||
@@ -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
|
||||
```
|
||||
@@ -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 |
|
||||
@@ -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)
|
||||
@@ -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 |
|
||||
@@ -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
|
||||
```
|
||||
@@ -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()
|
||||
@@ -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"
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user