chore: Remove outdated documentation files
- Deleted several obsolete documentation files related to API design, API gateway patterns, API versioning strategies, caching patterns, CI/CD advanced patterns, bilingual code comments, configuration management, data consistency patterns, Prisma database patterns, Kubernetes deployment patterns, and more. - This cleanup helps streamline the documentation and focus on current practices and standards, ensuring that only relevant and up-to-date information is maintained.
This commit is contained in:
@@ -1,171 +0,0 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
# RESTful API Design Standards
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use this skill when:
|
||||
- Creating new API endpoints
|
||||
- Designing request/response DTOs
|
||||
- Implementing controllers and routes
|
||||
- Writing OpenAPI/Swagger documentation
|
||||
- Standardizing error responses
|
||||
- Implementing pagination, filtering, and sorting
|
||||
- Setting up API versioning
|
||||
|
||||
## Core Principles
|
||||
|
||||
1. **Consistency**: All APIs follow the same patterns
|
||||
2. **Predictability**: Developers can guess endpoint behavior
|
||||
3. **Simplicity**: Easy to understand and use
|
||||
4. **Documentation**: Self-documenting through OpenAPI
|
||||
5. **Error Handling**: Clear, actionable error messages
|
||||
|
||||
## URL Structure
|
||||
|
||||
```
|
||||
https://api.goodgo.com/v1/{resource}/{id}/{sub-resource}
|
||||
|
||||
GET /v1/users # List users
|
||||
POST /v1/users # Create user
|
||||
GET /v1/users/123 # Get user by ID
|
||||
PUT /v1/users/123 # Update user
|
||||
DELETE /v1/users/123 # Delete user
|
||||
GET /v1/users/123/orders # Get user's orders
|
||||
```
|
||||
|
||||
## HTTP Methods
|
||||
|
||||
- **GET**: Retrieve resource(s) - Safe, Idempotent
|
||||
- **POST**: Create new resource - Not idempotent
|
||||
- **PUT**: Full update - Idempotent
|
||||
- **PATCH**: Partial update - Idempotent
|
||||
- **DELETE**: Remove resource - Idempotent
|
||||
|
||||
## Standard Response Format
|
||||
|
||||
```typescript
|
||||
// Success
|
||||
interface SuccessResponse<T> {
|
||||
success: true;
|
||||
data: T;
|
||||
pagination?: { page: number; limit: number; total: number; totalPages: number };
|
||||
}
|
||||
|
||||
// Error
|
||||
interface ErrorResponse {
|
||||
success: false;
|
||||
error: { code: string; message: string; details?: any; field?: string };
|
||||
}
|
||||
```
|
||||
|
||||
## Key Patterns
|
||||
|
||||
### Request DTO
|
||||
|
||||
```typescript
|
||||
export class CreateUserDto {
|
||||
@IsEmail() @IsNotEmpty() email: string;
|
||||
@MinLength(6) @IsNotEmpty() password: string;
|
||||
@IsOptional() name?: string;
|
||||
}
|
||||
|
||||
export class QueryUsersDto {
|
||||
@IsOptional() @Min(1) page?: number = 1;
|
||||
@IsOptional() @Min(1) @Max(100) limit?: number = 10;
|
||||
@IsOptional() search?: string;
|
||||
@IsOptional() @IsIn(['createdAt', 'name']) sortBy?: string = 'createdAt';
|
||||
@IsOptional() @IsIn(['asc', 'desc']) order?: 'asc' | 'desc' = 'desc';
|
||||
}
|
||||
```
|
||||
|
||||
### Controller
|
||||
|
||||
```typescript
|
||||
@Get()
|
||||
async list(@Query() query: QueryUsersDto) {
|
||||
const { data, total } = await this.userService.findAll(query);
|
||||
return {
|
||||
success: true,
|
||||
data: data.map(UserResponseDto.fromEntity),
|
||||
pagination: { page: query.page, limit: query.limit, total, totalPages: Math.ceil(total / query.limit) }
|
||||
};
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
async getById(@Param('id') id: string) {
|
||||
const user = await this.userService.findById(id);
|
||||
if (!user) throw new NotFoundException({ success: false, error: { code: 'NOT_FOUND', message: 'User not found' } });
|
||||
return { success: true, data: UserResponseDto.fromEntity(user) };
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
- **Resource Naming**: Use plural nouns (`/users`), kebab-case for multi-word
|
||||
- **Versioning**: Include version in URL (`/v1/users`), maintain backward compatibility
|
||||
- **Security**: Use HTTPS, implement rate limiting, validate all inputs
|
||||
- **Performance**: Implement pagination, use field filtering, cache responses
|
||||
- **Documentation**: Keep OpenAPI spec up to date, include examples
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
1. **Using Verbs in URLs**: Non-RESTful endpoints
|
||||
```
|
||||
# BAD: POST /api/v1/createUser
|
||||
# GOOD: POST /api/v1/users
|
||||
```
|
||||
|
||||
2. **Inconsistent Response Format**: Different structures for different endpoints
|
||||
```typescript
|
||||
// BAD: res.json({ user: data }) vs res.json({ result: data })
|
||||
// GOOD: res.json({ success: true, data })
|
||||
```
|
||||
|
||||
3. **Wrong HTTP Status Codes**: Using 200 for errors
|
||||
```typescript
|
||||
// BAD: res.status(200).json({ error: 'Not found' });
|
||||
// GOOD: res.status(404).json({ success: false, error: { code: 'NOT_FOUND' } });
|
||||
```
|
||||
|
||||
4. **Missing Pagination**: Returning all records
|
||||
```typescript
|
||||
// BAD: prisma.user.findMany()
|
||||
// GOOD: prisma.user.findMany({ skip: (page - 1) * limit, take: limit })
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| HTTP Method | Action | Idempotent | Status Codes |
|
||||
|-------------|--------|------------|--------------|
|
||||
| **GET** | Retrieve | Yes | 200, 404 |
|
||||
| **POST** | Create | No | 201, 400, 409 |
|
||||
| **PUT** | Full update | Yes | 200, 400, 404 |
|
||||
| **PATCH** | Partial update | Yes | 200, 400, 404 |
|
||||
| **DELETE** | Remove | Yes | 204, 404 |
|
||||
|
||||
**Response Format:**
|
||||
```typescript
|
||||
// Success: { success: true, data: T, pagination?: {...} }
|
||||
// Error: { success: false, error: { code: string, message: string } }
|
||||
```
|
||||
|
||||
**Common Error Codes:**
|
||||
- `400` - Bad Request (validation)
|
||||
- `401` - Unauthorized (no token)
|
||||
- `403` - Forbidden (no permission)
|
||||
- `404` - Not Found
|
||||
- `409` - Conflict (duplicate)
|
||||
- `422` - Unprocessable (business rule)
|
||||
- `429` - Rate limited
|
||||
|
||||
## Resources
|
||||
|
||||
- [OpenAPI Specification](https://spec.openapis.org/oas/latest.html) - Official OpenAPI docs
|
||||
- [REST API Design](https://restfulapi.net/) - REST best practices
|
||||
- [Detailed Code Examples](./references/REFERENCE.md)
|
||||
- [API Versioning Strategy](../api-versioning-strategy/SKILL.md) - Versioning patterns
|
||||
- [API Gateway Advanced](../api-gateway-advanced/SKILL.md) - Gateway patterns
|
||||
- [Middleware Patterns](../middleware-patterns/SKILL.md) - Request handling
|
||||
@@ -1,182 +0,0 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
# API Gateway Advanced Patterns
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use this skill when:
|
||||
- Implementing API composition and aggregation
|
||||
- Transforming requests/responses at gateway level
|
||||
- Integrating service mesh (Istio/Linkerd) with Traefik
|
||||
- Implementing advanced routing strategies
|
||||
- Adding gateway-level circuit breakers
|
||||
- Implementing API versioning at gateway
|
||||
- Optimizing gateway performance
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### API Gateway Responsibilities
|
||||
|
||||
1. **Request Routing**: Route requests to appropriate services
|
||||
2. **API Composition**: Aggregate multiple service responses
|
||||
3. **Protocol Translation**: Convert between protocols (HTTP, gRPC, GraphQL)
|
||||
4. **Request/Response Transformation**: Modify requests and responses
|
||||
5. **Security**: Authentication, authorization, rate limiting
|
||||
6. **Resilience**: Circuit breaker, retry, timeout at gateway level
|
||||
7. **Observability**: Logging, metrics, tracing
|
||||
|
||||
### API Composition Patterns
|
||||
|
||||
- **Aggregation**: Combine multiple service responses into single response
|
||||
- **Chaining**: Call services sequentially, use previous response in next call
|
||||
- **Fan-out/Fan-in**: Call multiple services in parallel, aggregate results
|
||||
|
||||
## Key Patterns
|
||||
|
||||
### API Composition
|
||||
|
||||
```typescript
|
||||
// Fan-out: Call multiple services in parallel
|
||||
async getUserProfile(userId: string) {
|
||||
const [user, orders, payments] = await Promise.all([
|
||||
this.userClient.get(`/users/${userId}`),
|
||||
this.orderClient.get(`/orders?userId=${userId}`),
|
||||
this.paymentClient.get(`/payments?userId=${userId}`),
|
||||
]);
|
||||
|
||||
return {
|
||||
user: user.data,
|
||||
orders: orders.data.orders,
|
||||
paymentHistory: payments.data.payments,
|
||||
};
|
||||
}
|
||||
|
||||
// Chaining: Sequential calls with compensation
|
||||
async createOrderWithPayment(data) {
|
||||
const order = await this.orderClient.post('/orders', data);
|
||||
try {
|
||||
const payment = await this.paymentClient.post('/payments', { orderId: order.data.id });
|
||||
return { order: order.data, payment: payment.data };
|
||||
} catch (error) {
|
||||
await this.orderClient.delete(`/orders/${order.data.id}`); // Compensate
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Gateway Caching
|
||||
|
||||
```typescript
|
||||
export function gatewayCache(ttl: number = 60) {
|
||||
return async (req, res, next) => {
|
||||
if (req.method !== 'GET') return next();
|
||||
|
||||
const cacheKey = `gateway:${req.path}:${JSON.stringify(req.query)}`;
|
||||
const cached = await cacheService.get(cacheKey);
|
||||
if (cached) return res.json(cached);
|
||||
|
||||
const originalJson = res.json.bind(res);
|
||||
res.json = (data) => {
|
||||
cacheService.set(cacheKey, data, ttl);
|
||||
return originalJson(data);
|
||||
};
|
||||
next();
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Traefik Circuit Breaker
|
||||
|
||||
```yaml
|
||||
middlewares:
|
||||
circuit-breaker:
|
||||
circuitBreaker:
|
||||
expression: "NetworkErrorRatio() > 0.50"
|
||||
timeout:
|
||||
forwardingTimeouts:
|
||||
dialTimeout: 5s
|
||||
responseHeaderTimeout: 10s
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
- **API Composition**: Use for aggregating related data from multiple services
|
||||
- **Caching**: Cache at gateway for frequently accessed data
|
||||
- **Circuit Breaker**: Implement at gateway to protect downstream services
|
||||
- **Transformation**: Keep transformations simple and testable
|
||||
- **Versioning**: Use path-based or header-based versioning
|
||||
- **Monitoring**: Monitor gateway metrics (latency, error rate)
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
1. **No Timeout at Gateway**: Requests hanging forever
|
||||
```yaml
|
||||
# GOOD: Set timeout
|
||||
middlewares:
|
||||
timeout:
|
||||
forwardingTimeouts:
|
||||
dialTimeout: 5s
|
||||
responseHeaderTimeout: 10s
|
||||
```
|
||||
|
||||
2. **Missing Circuit Breaker**: Cascading failures
|
||||
```yaml
|
||||
# GOOD: Add circuit breaker for each service
|
||||
middlewares:
|
||||
circuit-breaker:
|
||||
circuitBreaker:
|
||||
expression: "NetworkErrorRatio() > 0.50"
|
||||
```
|
||||
|
||||
3. **No Caching for Static Data**: Unnecessary service load
|
||||
```typescript
|
||||
// GOOD: Cache at gateway
|
||||
app.get('/api/config', gatewayCache(3600), handler);
|
||||
```
|
||||
|
||||
4. **N+1 API Calls from Client**: Multiple round trips
|
||||
```typescript
|
||||
// BAD: Client makes 3 calls
|
||||
// GOOD: Use API composition GET /user-profile/123
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Pattern | Use Case | Implementation |
|
||||
|---------|----------|----------------|
|
||||
| **API Composition** | Aggregate multiple services | Promise.all() |
|
||||
| **Request Transform** | Modify incoming requests | Middleware |
|
||||
| **Response Transform** | Standardize responses | Override res.json |
|
||||
| **Gateway Caching** | Cache GET responses | Redis/Memory |
|
||||
| **Circuit Breaker** | Protect from failures | Traefik middleware |
|
||||
|
||||
**Traefik Middleware Order:**
|
||||
```
|
||||
1. Rate Limiting
|
||||
2. Authentication
|
||||
3. Circuit Breaker
|
||||
4. Retry
|
||||
5. Headers Transform
|
||||
6. Compression
|
||||
```
|
||||
|
||||
**API Composition Patterns:**
|
||||
```typescript
|
||||
// Fan-out (parallel)
|
||||
const [users, orders] = await Promise.all([userClient.get('/users'), orderClient.get('/orders')]);
|
||||
|
||||
// Chaining (sequential)
|
||||
const order = await orderClient.create(data);
|
||||
const payment = await paymentClient.process(order.id);
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
- [Traefik Documentation](https://doc.traefik.io/traefik/) - Official Traefik docs
|
||||
- [API Gateway Pattern](https://microservices.io/patterns/apigateway.html) - Gateway patterns
|
||||
- [Service Mesh](https://istio.io/) - Istio service mesh
|
||||
- [Detailed Code Examples](./references/REFERENCE.md)
|
||||
- [Middleware Patterns](../middleware-patterns/SKILL.md) - Middleware patterns
|
||||
- [Resilience Patterns](../resilience-patterns/SKILL.md) - Circuit breaker patterns
|
||||
@@ -1,432 +0,0 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
# API Versioning Strategy
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use this skill when:
|
||||
- Versioning APIs
|
||||
- Handling breaking changes
|
||||
- Implementing API deprecation
|
||||
- Maintaining backward compatibility
|
||||
- Implementing version negotiation
|
||||
- Managing multiple API versions
|
||||
- Planning API evolution
|
||||
- Communicating API changes to consumers
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Versioning Strategies
|
||||
|
||||
1. **URL Path Versioning**: `/api/v1/users`, `/api/v2/users`
|
||||
2. **Header Versioning**: `Accept: application/vnd.goodgo.v1+json`
|
||||
3. **Query Parameter**: `/api/users?version=1`
|
||||
4. **Semantic Versioning**: Major.Minor.Patch (e.g., 1.2.3)
|
||||
|
||||
### Compatibility Types
|
||||
|
||||
- **Backward Compatible**: New version works with old clients
|
||||
- **Forward Compatible**: Old version works with new clients
|
||||
- **Breaking Changes**: Incompatible changes requiring new version
|
||||
|
||||
## URL Path Versioning
|
||||
|
||||
### Implementation
|
||||
|
||||
```typescript
|
||||
// src/routes/index.ts
|
||||
// EN: Route versioning
|
||||
// VI: Route versioning
|
||||
import { Router } from 'express';
|
||||
import v1Router from './v1';
|
||||
import v2Router from './v2';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// EN: Version 1 routes
|
||||
// VI: Routes version 1
|
||||
router.use('/v1', v1Router);
|
||||
|
||||
// EN: Version 2 routes
|
||||
// VI: Routes version 2
|
||||
router.use('/v2', v2Router);
|
||||
|
||||
export default router;
|
||||
```
|
||||
|
||||
### Version Router
|
||||
|
||||
```typescript
|
||||
// src/routes/v1/index.ts
|
||||
// EN: Version 1 routes
|
||||
// VI: Routes version 1
|
||||
import { Router } from 'express';
|
||||
import userRoutes from './users';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.use('/users', userRoutes);
|
||||
|
||||
export default router;
|
||||
|
||||
// src/routes/v2/index.ts
|
||||
// EN: Version 2 routes with breaking changes
|
||||
// VI: Routes version 2 với breaking changes
|
||||
import { Router } from 'express';
|
||||
import userRoutes from './users'; // EN: Different implementation / VI: Implementation khác
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.use('/users', userRoutes); // EN: Different response format / VI: Format response khác
|
||||
|
||||
export default router;
|
||||
```
|
||||
|
||||
## Header-Based Versioning
|
||||
|
||||
### Version Negotiation Middleware
|
||||
|
||||
```typescript
|
||||
// src/middlewares/version-negotiation.middleware.ts
|
||||
// EN: Version negotiation middleware
|
||||
// VI: Middleware version negotiation
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { logger } from '@goodgo/logger';
|
||||
|
||||
export function versionNegotiation(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): void {
|
||||
// EN: Extract version from Accept header
|
||||
// VI: Trích xuất version từ Accept header
|
||||
const acceptHeader = req.headers.accept || '';
|
||||
const versionMatch = acceptHeader.match(/application\/vnd\.goodgo\.v(\d+)\+json/);
|
||||
|
||||
if (versionMatch) {
|
||||
const requestedVersion = parseInt(versionMatch[1], 10);
|
||||
req.apiVersion = requestedVersion;
|
||||
|
||||
// EN: Check if version is supported
|
||||
// VI: Kiểm tra xem version có được hỗ trợ không
|
||||
const supportedVersions = [1, 2];
|
||||
if (!supportedVersions.includes(requestedVersion)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: {
|
||||
code: 'UNSUPPORTED_VERSION',
|
||||
message: `API version ${requestedVersion} is not supported. Supported versions: ${supportedVersions.join(', ')}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// EN: Default to latest version
|
||||
// VI: Mặc định version mới nhất
|
||||
req.apiVersion = 2;
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
```
|
||||
|
||||
### Version-Aware Controller
|
||||
|
||||
```typescript
|
||||
// src/modules/user/user.controller.ts
|
||||
// EN: Version-aware controller
|
||||
// VI: Controller nhận biết version
|
||||
export class UserController {
|
||||
async getUser(req: Request, res: Response): Promise<void> {
|
||||
const version = req.apiVersion || 2;
|
||||
|
||||
if (version === 1) {
|
||||
// EN: Version 1 response format
|
||||
// VI: Format response version 1
|
||||
const user = await this.userService.findById(req.params.id);
|
||||
res.json({
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
});
|
||||
} else {
|
||||
// EN: Version 2 response format
|
||||
// VI: Format response version 2
|
||||
const user = await this.userService.findById(req.params.id);
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
profile: user.profile, // EN: New field in v2 / VI: Field mới trong v2
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
version: '2.0.0',
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Semantic Versioning
|
||||
|
||||
### Version Structure
|
||||
|
||||
```
|
||||
MAJOR.MINOR.PATCH
|
||||
|
||||
MAJOR: Breaking changes
|
||||
MINOR: Backward-compatible additions
|
||||
PATCH: Backward-compatible bug fixes
|
||||
```
|
||||
|
||||
### Version Response
|
||||
|
||||
```typescript
|
||||
// src/core/api/version.middleware.ts
|
||||
// EN: Add version to response
|
||||
// VI: Thêm version vào response
|
||||
export function versionMiddleware(req: Request, res: Response, next: NextFunction): void {
|
||||
const originalJson = res.json.bind(res);
|
||||
|
||||
res.json = (data: any) => {
|
||||
const response = {
|
||||
...data,
|
||||
metadata: {
|
||||
...data.metadata,
|
||||
apiVersion: req.apiVersion || '2.0.0',
|
||||
serviceVersion: process.env.SERVICE_VERSION || '1.0.0',
|
||||
},
|
||||
};
|
||||
|
||||
return originalJson(response);
|
||||
};
|
||||
|
||||
next();
|
||||
}
|
||||
```
|
||||
|
||||
## API Deprecation
|
||||
|
||||
### Deprecation Headers
|
||||
|
||||
```typescript
|
||||
// src/middlewares/deprecation.middleware.ts
|
||||
// EN: Deprecation warning middleware
|
||||
// VI: Middleware cảnh báo deprecation
|
||||
export function deprecationMiddleware(version: string, sunsetDate: string) {
|
||||
return (req: Request, res: Response, next: NextFunction): void => {
|
||||
if (req.apiVersion && parseInt(req.apiVersion.toString()) < parseInt(version)) {
|
||||
res.setHeader('Deprecation', 'true');
|
||||
res.setHeader('Sunset', sunsetDate);
|
||||
res.setHeader('Link', `<${req.url.replace(/\/v\d+/, `/v${version}`)}>; rel="successor-version"`);
|
||||
res.setHeader('Warning', `299 - "API version ${req.apiVersion} is deprecated. Please migrate to version ${version} by ${sunsetDate}"`);
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
// Usage
|
||||
router.use('/v1', deprecationMiddleware('2', '2024-12-31'), v1Router);
|
||||
```
|
||||
|
||||
### Deprecation Documentation
|
||||
|
||||
```typescript
|
||||
// src/docs/deprecation.md
|
||||
// EN: Deprecation notices
|
||||
// VI: Thông báo deprecation
|
||||
|
||||
## API Version 1 Deprecation
|
||||
|
||||
**Deprecated**: 2024-01-01
|
||||
**Sunset Date**: 2024-12-31
|
||||
**Migration Guide**: [Migration Guide](./migration-v1-to-v2.md)
|
||||
|
||||
### Changes in Version 2
|
||||
|
||||
- New response format
|
||||
- Additional user profile fields
|
||||
- Updated authentication flow
|
||||
```
|
||||
|
||||
## Backward Compatibility
|
||||
|
||||
### Compatibility Layer
|
||||
|
||||
```typescript
|
||||
// src/core/api/compatibility.adapter.ts
|
||||
// EN: Backward compatibility adapter
|
||||
// VI: Adapter tương thích ngược
|
||||
export class CompatibilityAdapter {
|
||||
/**
|
||||
* EN: Adapt v1 response to v2 format
|
||||
* VI: Adapt response v1 sang format v2
|
||||
*/
|
||||
adaptV1ToV2(v1Data: any): any {
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
user: {
|
||||
...v1Data,
|
||||
profile: null, // EN: Add default for new field / VI: Thêm mặc định cho field mới
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
version: '2.0.0',
|
||||
adapted: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* EN: Adapt v2 request to v1 format
|
||||
* VI: Adapt request v2 sang format v1
|
||||
*/
|
||||
adaptV2RequestToV1(v2Request: any): any {
|
||||
return {
|
||||
email: v2Request.email,
|
||||
name: v2Request.name,
|
||||
// EN: Ignore new fields / VI: Bỏ qua các field mới
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Breaking Changes Migration
|
||||
|
||||
### Migration Strategy
|
||||
|
||||
```typescript
|
||||
// src/core/api/migration.strategy.ts
|
||||
// EN: Migration strategy for breaking changes
|
||||
// VI: Chiến lược migration cho breaking changes
|
||||
export class MigrationStrategy {
|
||||
/**
|
||||
* EN: Phase 1: Support both versions
|
||||
* VI: Giai đoạn 1: Hỗ trợ cả hai versions
|
||||
*/
|
||||
phase1SupportBoth(): void {
|
||||
// EN: Keep v1, add v2
|
||||
// VI: Giữ v1, thêm v2
|
||||
router.use('/v1', v1Router);
|
||||
router.use('/v2', v2Router);
|
||||
}
|
||||
|
||||
/**
|
||||
* EN: Phase 2: Deprecate v1
|
||||
* VI: Giai đoạn 2: Deprecate v1
|
||||
*/
|
||||
phase2DeprecateV1(): void {
|
||||
router.use('/v1', deprecationMiddleware('2', '2024-12-31'), v1Router);
|
||||
router.use('/v2', v2Router);
|
||||
}
|
||||
|
||||
/**
|
||||
* EN: Phase 3: Remove v1
|
||||
* VI: Giai đoạn 3: Xóa v1
|
||||
*/
|
||||
phase3RemoveV1(): void {
|
||||
// EN: After sunset date, remove v1 routes
|
||||
// VI: Sau sunset date, xóa v1 routes
|
||||
router.use('/v2', v2Router);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Versioning Strategy**: Choose URL path or header, be consistent
|
||||
2. **Semantic Versioning**: Use MAJOR.MINOR.PATCH
|
||||
3. **Deprecation**: Always deprecate before removing
|
||||
4. **Migration Guide**: Provide clear migration documentation
|
||||
5. **Backward Compatibility**: Maintain compatibility when possible
|
||||
6. **Communication**: Clearly communicate version changes
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
1. **No Deprecation Period**: Breaking clients suddenly
|
||||
```typescript
|
||||
// ❌ BAD: Remove v1 immediately
|
||||
router.use('/v2', v2Router);
|
||||
|
||||
// ✅ GOOD: Deprecate with sunset date
|
||||
router.use('/v1', deprecationMiddleware('2', '2024-12-31'), v1Router);
|
||||
router.use('/v2', v2Router);
|
||||
```
|
||||
|
||||
2. **Breaking Changes Without Major Version**: Client confusion
|
||||
```
|
||||
# ❌ BAD: Breaking change in minor version
|
||||
v1.1.0 → Changed response format
|
||||
|
||||
# ✅ GOOD: Breaking change = new major version
|
||||
v1.x.x → v2.0.0 (new response format)
|
||||
```
|
||||
|
||||
3. **Inconsistent Versioning Strategy**: Mixed approaches
|
||||
```typescript
|
||||
// ❌ BAD: Mix URL and header versioning
|
||||
/api/v1/users + Accept: application/vnd.v2+json
|
||||
|
||||
// ✅ GOOD: Choose one approach
|
||||
/api/v1/users OR Accept: application/vnd.goodgo.v1+json
|
||||
```
|
||||
|
||||
4. **No Migration Guide**: Clients don't know how to upgrade
|
||||
```
|
||||
# ✅ Always provide:
|
||||
- Changelog
|
||||
- Migration guide
|
||||
- Sunset date
|
||||
- Deprecation warnings
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Strategy | Pros | Cons | Use When |
|
||||
|----------|------|------|----------|
|
||||
| **URL Path** | Clear, cacheable | URL changes | Public APIs |
|
||||
| **Header** | Clean URLs | Less visible | Internal APIs |
|
||||
| **Query Param** | Simple | Not RESTful | Quick prototypes |
|
||||
|
||||
**Semantic Versioning:**
|
||||
```
|
||||
MAJOR.MINOR.PATCH
|
||||
│ │ └── Bug fixes (backward compatible)
|
||||
│ └──────── New features (backward compatible)
|
||||
└────────────── Breaking changes
|
||||
```
|
||||
|
||||
**Version Lifecycle:**
|
||||
```
|
||||
v1 Active → v2 Released → v1 Deprecated → v1 Sunset → v1 Removed
|
||||
│ │ │ │ │
|
||||
│ │ Add headers Remove from Delete
|
||||
│ Support + warnings docs routes
|
||||
Solo both
|
||||
```
|
||||
|
||||
**Deprecation Headers:**
|
||||
```http
|
||||
Deprecation: true
|
||||
Sunset: Sat, 31 Dec 2024 23:59:59 GMT
|
||||
Warning: 299 - "API v1 is deprecated. Migrate to v2 by 2024-12-31"
|
||||
Link: </api/v2/users>; rel="successor-version"
|
||||
```
|
||||
|
||||
**Version Detection:**
|
||||
```typescript
|
||||
// URL path
|
||||
const version = req.path.match(/\/v(\d+)\//)?.[1] || '2';
|
||||
|
||||
// Header
|
||||
const version = req.headers.accept?.match(/vnd\.goodgo\.v(\d+)/)?.[1];
|
||||
```
|
||||
@@ -1,325 +0,0 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
# Caching Patterns
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use this skill when:
|
||||
- Implementing caching for frequently accessed data
|
||||
- Optimizing database queries with caching
|
||||
- Designing cache key naming conventions
|
||||
- Setting TTL (Time To Live) strategies
|
||||
- Implementing cache invalidation patterns
|
||||
- Using multi-layer cache (L1: Memory, L2: Redis)
|
||||
- Handling cache failures gracefully
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Multi-Layer Cache Strategy
|
||||
|
||||
The platform uses a two-layer cache architecture:
|
||||
|
||||
1. **L1 Cache (Memory)**: NodeCache in-memory cache
|
||||
- Very fast (< 1ms access time)
|
||||
- Limited capacity (10k keys default)
|
||||
- Short TTL (60 seconds default, max 5 minutes)
|
||||
- Per-instance (not shared across instances)
|
||||
|
||||
2. **L2 Cache (Redis)**: Distributed Redis cache
|
||||
- Fast (< 5ms access time)
|
||||
- Large capacity
|
||||
- Longer TTL (configurable)
|
||||
- Shared across all service instances
|
||||
|
||||
### Cache Flow
|
||||
|
||||
```
|
||||
Request → L1 Cache → Hit? Return
|
||||
↓ Miss
|
||||
L2 Cache → Hit? Return + Warm L1
|
||||
↓ Miss
|
||||
Data Source (DB/API) → Store in L1 & L2 → Return
|
||||
```
|
||||
|
||||
## Patterns
|
||||
|
||||
### Cache Service Usage
|
||||
|
||||
```typescript
|
||||
import { cacheService } from '../core/cache';
|
||||
|
||||
// Simple get/set
|
||||
const cached = await cacheService.get<User>('user:123');
|
||||
await cacheService.set('user:123', userData, 300); // 5 minutes TTL
|
||||
|
||||
// Get or set pattern (cache-aside)
|
||||
const user = await cacheService.getOrSet(
|
||||
'user:123',
|
||||
async () => {
|
||||
return await userRepository.findById('123');
|
||||
},
|
||||
300 // TTL in seconds
|
||||
);
|
||||
```
|
||||
|
||||
### Cache Key Naming Conventions
|
||||
|
||||
Use consistent naming patterns:
|
||||
|
||||
```typescript
|
||||
// Pattern: {entity}:{identifier}
|
||||
'user:123'
|
||||
'user:email:user@example.com'
|
||||
'user:123:permissions'
|
||||
'user:123:roles'
|
||||
|
||||
// Pattern: {entity}:{identifier}:{sub-resource}
|
||||
'session:abc123'
|
||||
'permission:perm_123'
|
||||
'role:role_123'
|
||||
```
|
||||
|
||||
Cache service provides key generators:
|
||||
|
||||
```typescript
|
||||
cacheService.keys = {
|
||||
user: (userId: string) => `user:${userId}`,
|
||||
userPermissions: (userId: string) => `user:${userId}:permissions`,
|
||||
userRoles: (userId: string) => `user:${userId}:roles`,
|
||||
token: (token: string) => `token:${token}`,
|
||||
session: (sessionId: string) => `session:${sessionId}`,
|
||||
};
|
||||
```
|
||||
|
||||
### Cache-Aside Pattern
|
||||
|
||||
Most common pattern - check cache first, fetch if miss:
|
||||
|
||||
```typescript
|
||||
async getUserPermissions(userId: string): Promise<string[]> {
|
||||
const cacheKey = cacheService.keys.userPermissions(userId);
|
||||
|
||||
// Try cache first
|
||||
const cached = await cacheService.get<string[]>(cacheKey);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
// Cache miss - fetch from source
|
||||
const permissions = await calculatePermissions(userId);
|
||||
|
||||
// Store in cache
|
||||
await cacheService.set(cacheKey, permissions, 300); // 5 min TTL
|
||||
|
||||
return permissions;
|
||||
}
|
||||
```
|
||||
|
||||
### Get or Set Pattern
|
||||
|
||||
Simplified cache-aside pattern:
|
||||
|
||||
```typescript
|
||||
const permissions = await cacheService.getOrSet(
|
||||
cacheService.keys.userPermissions(userId),
|
||||
async () => {
|
||||
// This function only runs on cache miss
|
||||
return await calculatePermissions(userId);
|
||||
},
|
||||
300 // TTL
|
||||
);
|
||||
```
|
||||
|
||||
### TTL Strategies
|
||||
|
||||
Choose TTL based on data characteristics:
|
||||
|
||||
**Short TTL (60-300s)**: Frequently changing data
|
||||
- User permissions (300s)
|
||||
- Session data (varies)
|
||||
- Real-time statistics
|
||||
|
||||
**Medium TTL (300-1800s)**: Moderately changing data
|
||||
- User profiles (600s)
|
||||
- Organization data (900s)
|
||||
- Configuration (1800s)
|
||||
|
||||
**Long TTL (1800-3600s)**: Rarely changing data
|
||||
- Static configurations (3600s)
|
||||
- Reference data (7200s)
|
||||
|
||||
**No TTL**: Very stable data (use with caution)
|
||||
- Rarely use - prefer long TTL instead
|
||||
|
||||
### Cache Invalidation
|
||||
|
||||
Invalidate cache when data changes:
|
||||
|
||||
```typescript
|
||||
// Single key invalidation
|
||||
await cacheService.del(cacheService.keys.user(userId));
|
||||
await cacheService.del(cacheService.keys.userPermissions(userId));
|
||||
|
||||
// Pattern-based invalidation
|
||||
await cacheService.invalidatePattern('user:123:*');
|
||||
|
||||
// Multiple keys
|
||||
await cacheService.delMany([
|
||||
cacheService.keys.user(userId),
|
||||
cacheService.keys.userPermissions(userId),
|
||||
cacheService.keys.userRoles(userId),
|
||||
]);
|
||||
```
|
||||
|
||||
### Cache Warming
|
||||
|
||||
Pre-populate cache with frequently accessed data:
|
||||
|
||||
```typescript
|
||||
async warmCache() {
|
||||
const activeUsers = await userRepository.findActiveUsers();
|
||||
|
||||
for (const user of activeUsers) {
|
||||
// Pre-cache user data
|
||||
await cacheService.set(
|
||||
cacheService.keys.user(user.id),
|
||||
user,
|
||||
600
|
||||
);
|
||||
|
||||
// Pre-cache permissions
|
||||
const permissions = await calculatePermissions(user.id);
|
||||
await cacheService.set(
|
||||
cacheService.keys.userPermissions(user.id),
|
||||
permissions,
|
||||
300
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
Cache failures should not break the application:
|
||||
|
||||
```typescript
|
||||
async getWithCache(key: string): Promise<Data | null> {
|
||||
try {
|
||||
// Try cache first
|
||||
const cached = await cacheService.get<Data>(key);
|
||||
if (cached) return cached;
|
||||
} catch (error) {
|
||||
// Log but continue - fallback to source
|
||||
logger.warn('Cache get failed, falling back to source', { key, error });
|
||||
}
|
||||
|
||||
// Fallback to data source
|
||||
return await fetchFromSource();
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Cache Keys**: Use consistent naming conventions
|
||||
2. **TTL Selection**: Choose TTL based on data change frequency
|
||||
3. **Invalidation**: Invalidate cache when data changes
|
||||
4. **Error Handling**: Don't let cache failures break the app
|
||||
5. **Cache-Aside**: Use cache-aside pattern for most cases
|
||||
6. **Avoid Over-Caching**: Don't cache data that changes too frequently
|
||||
7. **Monitor Hit Rates**: Track cache hit rates to optimize TTL
|
||||
8. **Warm Cache**: Pre-populate cache for critical data
|
||||
9. **Use Multi-Layer**: Leverage both L1 and L2 cache
|
||||
10. **Serialize Properly**: Ensure data is JSON serializable
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
1. **Cache Key Collisions**: Using generic keys that collide
|
||||
2. **Stale Data**: Not invalidating cache when data changes
|
||||
3. **Too Short TTL**: Setting TTL too short, negating cache benefits
|
||||
4. **Too Long TTL**: Setting TTL too long, serving stale data
|
||||
5. **No Error Handling**: Letting cache errors break the application
|
||||
6. **Caching Everything**: Caching data that doesn't benefit from caching
|
||||
7. **Not Warming Cache**: Not pre-populating critical cache data
|
||||
8. **Ignoring Hit Rates**: Not monitoring cache performance
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Low Cache Hit Rate
|
||||
|
||||
**Problem**: Cache hit rate is low, cache not effective
|
||||
**Solution**:
|
||||
- Review TTL values - may be too short
|
||||
- Check cache key patterns - ensure consistent usage
|
||||
- Verify cache invalidation isn't too aggressive
|
||||
- Monitor what data is being cached
|
||||
|
||||
### Stale Data Issues
|
||||
|
||||
**Problem**: Serving stale data from cache
|
||||
**Solution**:
|
||||
- Review TTL values - may be too long
|
||||
- Ensure cache invalidation on data updates
|
||||
- Use shorter TTL for frequently changing data
|
||||
- Implement cache versioning if needed
|
||||
|
||||
### Cache Performance Issues
|
||||
|
||||
**Problem**: Cache operations are slow
|
||||
**Solution**:
|
||||
- Check Redis connection and network latency
|
||||
- Monitor Redis memory usage
|
||||
- Review cache key patterns for efficiency
|
||||
- Consider L1 cache hit rate (should be high)
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Cache Layer | Speed | Capacity | TTL | Scope |
|
||||
|-------------|-------|----------|-----|-------|
|
||||
| **L1 (Memory)** | <1ms | 10k keys | 60s-5min | Per instance |
|
||||
| **L2 (Redis)** | <5ms | Large | Configurable | Shared |
|
||||
|
||||
**TTL Guidelines:**
|
||||
| Data Type | TTL | Example |
|
||||
|-----------|-----|---------|
|
||||
| Session data | 15-60min | User sessions |
|
||||
| Permissions | 5min | RBAC cache |
|
||||
| User profiles | 10min | Profile data |
|
||||
| Config | 30min | Feature flags |
|
||||
| Static data | 1-2hr | Reference data |
|
||||
|
||||
**Key Patterns:**
|
||||
```typescript
|
||||
// Entity
|
||||
`user:${userId}`
|
||||
`session:${sessionId}`
|
||||
|
||||
// Entity + Sub-resource
|
||||
`user:${userId}:permissions`
|
||||
`user:${userId}:roles`
|
||||
|
||||
// List/Collection
|
||||
`users:list:page:${page}`
|
||||
`products:category:${categoryId}`
|
||||
```
|
||||
|
||||
**Essential Operations:**
|
||||
```typescript
|
||||
// Get/Set
|
||||
await cache.get<T>(key);
|
||||
await cache.set(key, value, ttl);
|
||||
|
||||
// Get or fetch
|
||||
await cache.getOrSet(key, fetchFn, ttl);
|
||||
|
||||
// Invalidate
|
||||
await cache.del(key);
|
||||
await cache.invalidatePattern('user:123:*');
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
- [Multi-Layer Cache](../../services/iam-service/src/core/cache/multi-layer-cache.ts) - Multi-layer cache implementation
|
||||
- [Cache Service](../../services/iam-service/src/core/cache/cache.service.ts) - Cache service wrapper
|
||||
- [Cache Usage Example](../../services/iam-service/src/modules/rbac/rbac.service.ts) - Real-world cache usage
|
||||
@@ -1,484 +0,0 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
# CI/CD Advanced Patterns
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use this skill when:
|
||||
- Implementing blue-green deployments
|
||||
- Setting up canary releases
|
||||
- Implementing automated rollback mechanisms
|
||||
- Creating deployment verification pipelines
|
||||
- Implementing progressive delivery
|
||||
- Setting up deployment gates
|
||||
- Implementing smoke tests
|
||||
- Managing deployment strategies in Kubernetes
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Deployment Strategies
|
||||
|
||||
1. **Rolling Update**: Gradual replacement (default K8s)
|
||||
2. **Blue-Green**: Two identical environments, switch traffic
|
||||
3. **Canary**: Gradual rollout to subset of users
|
||||
4. **Recreate**: Stop old, start new (downtime)
|
||||
|
||||
### Deployment Verification
|
||||
|
||||
- Smoke tests
|
||||
- Health checks
|
||||
- Performance tests
|
||||
- Rollback triggers
|
||||
|
||||
## Blue-Green Deployment
|
||||
|
||||
### Kubernetes Implementation
|
||||
|
||||
```yaml
|
||||
# deployments/production/kubernetes/user-service-blue.yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: user-service-blue
|
||||
labels:
|
||||
app: user-service
|
||||
version: blue
|
||||
spec:
|
||||
replicas: 3
|
||||
selector:
|
||||
matchLabels:
|
||||
app: user-service
|
||||
version: blue
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: user-service
|
||||
version: blue
|
||||
spec:
|
||||
containers:
|
||||
- name: user-service
|
||||
image: goodgo/user-service:v1.0.0
|
||||
ports:
|
||||
- containerPort: 5000
|
||||
|
||||
---
|
||||
# deployments/production/kubernetes/user-service-green.yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: user-service-green
|
||||
labels:
|
||||
app: user-service
|
||||
version: green
|
||||
spec:
|
||||
replicas: 3
|
||||
selector:
|
||||
matchLabels:
|
||||
app: user-service
|
||||
version: green
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: user-service
|
||||
version: green
|
||||
spec:
|
||||
containers:
|
||||
- name: user-service
|
||||
image: goodgo/user-service:v1.1.0
|
||||
ports:
|
||||
- containerPort: 5000
|
||||
|
||||
---
|
||||
# Service selector switches between blue/green
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: user-service
|
||||
spec:
|
||||
selector:
|
||||
app: user-service
|
||||
version: blue # EN: Switch to green after verification / VI: Chuyển sang green sau khi xác minh
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 5000
|
||||
```
|
||||
|
||||
### Blue-Green Switch Script
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# scripts/deployment/blue-green-switch.sh
|
||||
# EN: Switch between blue and green deployments
|
||||
# VI: Chuyển đổi giữa blue và green deployments
|
||||
|
||||
SERVICE_NAME=$1
|
||||
CURRENT_VERSION=$(kubectl get service $SERVICE_NAME -o jsonpath='{.spec.selector.version}')
|
||||
NEW_VERSION=$([ "$CURRENT_VERSION" = "blue" ] && echo "green" || echo "blue")
|
||||
|
||||
echo "Switching from $CURRENT_VERSION to $NEW_VERSION"
|
||||
|
||||
# EN: Update service selector
|
||||
# VI: Cập nhật service selector
|
||||
kubectl patch service $SERVICE_NAME -p "{\"spec\":{\"selector\":{\"version\":\"$NEW_VERSION\"}}}"
|
||||
|
||||
# EN: Wait for rollout
|
||||
# VI: Đợi rollout
|
||||
kubectl rollout status deployment/$SERVICE_NAME-$NEW_VERSION
|
||||
|
||||
# EN: Run smoke tests
|
||||
# VI: Chạy smoke tests
|
||||
./scripts/deployment/smoke-tests.sh $SERVICE_NAME
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Smoke tests failed, rolling back"
|
||||
kubectl patch service $SERVICE_NAME -p "{\"spec\":{\"selector\":{\"version\":\"$CURRENT_VERSION\"}}}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Successfully switched to $NEW_VERSION"
|
||||
```
|
||||
|
||||
## Canary Deployment
|
||||
|
||||
### Kubernetes Canary with Service Mesh
|
||||
|
||||
```yaml
|
||||
# deployments/production/kubernetes/user-service-canary.yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: user-service-canary
|
||||
labels:
|
||||
app: user-service
|
||||
version: canary
|
||||
spec:
|
||||
replicas: 1 # EN: Start with 1 replica (10% traffic) / VI: Bắt đầu với 1 replica (10% traffic)
|
||||
selector:
|
||||
matchLabels:
|
||||
app: user-service
|
||||
version: canary
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: user-service
|
||||
version: canary
|
||||
spec:
|
||||
containers:
|
||||
- name: user-service
|
||||
image: goodgo/user-service:v1.1.0
|
||||
|
||||
---
|
||||
# VirtualService splits traffic
|
||||
apiVersion: networking.istio.io/v1alpha3
|
||||
kind: VirtualService
|
||||
metadata:
|
||||
name: user-service
|
||||
spec:
|
||||
hosts:
|
||||
- user-service
|
||||
http:
|
||||
- match:
|
||||
- headers:
|
||||
canary:
|
||||
exact: "true"
|
||||
route:
|
||||
- destination:
|
||||
host: user-service
|
||||
subset: canary
|
||||
weight: 100
|
||||
- route:
|
||||
- destination:
|
||||
host: user-service
|
||||
subset: stable
|
||||
weight: 90
|
||||
- destination:
|
||||
host: user-service
|
||||
subset: canary
|
||||
weight: 10 # EN: 10% traffic to canary / VI: 10% traffic tới canary
|
||||
```
|
||||
|
||||
### Progressive Canary Rollout
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# scripts/deployment/canary-rollout.sh
|
||||
# EN: Progressive canary rollout
|
||||
# VI: Progressive canary rollout
|
||||
|
||||
SERVICE_NAME=$1
|
||||
CANARY_PERCENTAGES=(10 25 50 75 100)
|
||||
|
||||
for PERCENTAGE in "${CANARY_PERCENTAGES[@]}"; do
|
||||
echo "Rolling out to $PERCENTAGE%"
|
||||
|
||||
# EN: Update VirtualService weight
|
||||
# VI: Cập nhật VirtualService weight
|
||||
kubectl patch virtualservice $SERVICE_NAME -p "{\"spec\":{\"http\":[{\"route\":[{\"destination\":{\"subset\":\"canary\"},\"weight\":$PERCENTAGE},{\"destination\":{\"subset\":\"stable\"},\"weight\":$((100-PERCENTAGE))}]}]}}"
|
||||
|
||||
# EN: Wait for traffic to stabilize
|
||||
# VI: Đợi traffic ổn định
|
||||
sleep 60
|
||||
|
||||
# EN: Run health checks
|
||||
# VI: Chạy health checks
|
||||
./scripts/deployment/health-checks.sh $SERVICE_NAME
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Health checks failed at $PERCENTAGE%, rolling back"
|
||||
kubectl patch virtualservice $SERVICE_NAME -p "{\"spec\":{\"http\":[{\"route\":[{\"destination\":{\"subset\":\"canary\"},\"weight\":0},{\"destination\":{\"subset\":\"stable\"},\"weight\":100}]}]}}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Successfully rolled out to $PERCENTAGE%"
|
||||
done
|
||||
|
||||
echo "Canary rollout complete"
|
||||
```
|
||||
|
||||
## Automated Rollback
|
||||
|
||||
### Rollback Script
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# scripts/deployment/rollback.sh
|
||||
# EN: Automated rollback to previous version
|
||||
# VI: Rollback tự động về version trước
|
||||
|
||||
SERVICE_NAME=$1
|
||||
NAMESPACE=${2:-production}
|
||||
|
||||
# EN: Get previous deployment revision
|
||||
# VI: Lấy revision deployment trước
|
||||
PREVIOUS_REVISION=$(kubectl rollout history deployment/$SERVICE_NAME -n $NAMESPACE --no-headers | tail -1 | awk '{print $1}')
|
||||
|
||||
if [ -z "$PREVIOUS_REVISION" ]; then
|
||||
echo "No previous revision found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Rolling back to revision $PREVIOUS_REVISION"
|
||||
|
||||
# EN: Rollback deployment
|
||||
# VI: Rollback deployment
|
||||
kubectl rollout undo deployment/$SERVICE_NAME -n $NAMESPACE --to-revision=$PREVIOUS_REVISION
|
||||
|
||||
# EN: Wait for rollout
|
||||
# VI: Đợi rollout
|
||||
kubectl rollout status deployment/$SERVICE_NAME -n $NAMESPACE
|
||||
|
||||
echo "Rollback complete"
|
||||
```
|
||||
|
||||
### Automated Rollback on Failure
|
||||
|
||||
```yaml
|
||||
# .github/workflows/deploy-production.yml
|
||||
name: Deploy Production
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Deploy to Kubernetes
|
||||
run: |
|
||||
kubectl apply -f deployments/production/kubernetes/
|
||||
kubectl rollout status deployment/user-service
|
||||
|
||||
- name: Run Smoke Tests
|
||||
run: ./scripts/deployment/smoke-tests.sh user-service
|
||||
|
||||
- name: Rollback on Failure
|
||||
if: failure()
|
||||
run: ./scripts/deployment/rollback.sh user-service production
|
||||
```
|
||||
|
||||
## Deployment Verification
|
||||
|
||||
### Smoke Tests
|
||||
|
||||
```typescript
|
||||
// scripts/deployment/smoke-tests.ts
|
||||
// EN: Smoke tests for deployment verification
|
||||
// VI: Smoke tests để xác minh deployment
|
||||
import axios from 'axios';
|
||||
|
||||
const SERVICE_URL = process.env.SERVICE_URL || 'http://localhost';
|
||||
|
||||
async function runSmokeTests(): Promise<boolean> {
|
||||
try {
|
||||
// EN: Health check
|
||||
// VI: Health check
|
||||
const healthResponse = await axios.get(`${SERVICE_URL}/health`);
|
||||
if (healthResponse.status !== 200) {
|
||||
console.error('Health check failed');
|
||||
return false;
|
||||
}
|
||||
|
||||
// EN: Basic functionality test
|
||||
// VI: Test chức năng cơ bản
|
||||
const testResponse = await axios.get(`${SERVICE_URL}/api/v1/users`, {
|
||||
timeout: 5000,
|
||||
});
|
||||
|
||||
if (testResponse.status !== 200) {
|
||||
console.error('Functionality test failed');
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log('Smoke tests passed');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Smoke tests failed', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
runSmokeTests().then((success) => {
|
||||
process.exit(success ? 0 : 1);
|
||||
});
|
||||
```
|
||||
|
||||
### Health Check Script
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# scripts/deployment/health-checks.sh
|
||||
# EN: Comprehensive health checks
|
||||
# VI: Health checks toàn diện
|
||||
|
||||
SERVICE_NAME=$1
|
||||
NAMESPACE=${2:-production}
|
||||
|
||||
echo "Running health checks for $SERVICE_NAME"
|
||||
|
||||
# EN: Check pods are ready
|
||||
# VI: Kiểm tra pods đã ready
|
||||
READY_PODS=$(kubectl get pods -n $NAMESPACE -l app=$SERVICE_NAME --field-selector=status.phase=Running --no-headers | wc -l)
|
||||
|
||||
if [ $READY_PODS -eq 0 ]; then
|
||||
echo "No ready pods found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# EN: Check service endpoints
|
||||
# VI: Kiểm tra service endpoints
|
||||
ENDPOINTS=$(kubectl get endpoints $SERVICE_NAME -n $NAMESPACE -o jsonpath='{.subsets[0].addresses[*].ip}' | wc -w)
|
||||
|
||||
if [ $ENDPOINTS -eq 0 ]; then
|
||||
echo "No service endpoints found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# EN: Check health endpoint
|
||||
# VI: Kiểm tra health endpoint
|
||||
SERVICE_URL=$(kubectl get service $SERVICE_NAME -n $NAMESPACE -o jsonpath='{.status.loadBalancer.ingress[0].hostname}')
|
||||
|
||||
if [ -z "$SERVICE_URL" ]; then
|
||||
SERVICE_URL="http://$SERVICE_NAME.$NAMESPACE.svc.cluster.local"
|
||||
fi
|
||||
|
||||
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" $SERVICE_URL/health)
|
||||
|
||||
if [ $HTTP_CODE -ne 200 ]; then
|
||||
echo "Health endpoint returned $HTTP_CODE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Health checks passed"
|
||||
```
|
||||
|
||||
## Deployment Gates
|
||||
|
||||
```yaml
|
||||
# .github/workflows/deploy-with-gates.yml
|
||||
name: Deploy with Gates
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
steps:
|
||||
- name: Deploy
|
||||
run: kubectl apply -f deployments/
|
||||
|
||||
- name: Wait for Rollout
|
||||
run: kubectl rollout status deployment/service
|
||||
|
||||
- name: Smoke Tests Gate
|
||||
id: smoke-tests
|
||||
run: ./scripts/deployment/smoke-tests.sh
|
||||
|
||||
- name: Performance Tests Gate
|
||||
if: steps.smoke-tests.outcome == 'success'
|
||||
run: ./scripts/deployment/performance-tests.sh
|
||||
|
||||
- name: Manual Approval Gate
|
||||
if: steps.smoke-tests.outcome == 'success'
|
||||
uses: trstringer/manual-approval@v1
|
||||
with:
|
||||
secret: ${{ secrets.GITHUB_TOKEN }}
|
||||
approvers: team-leads
|
||||
minimum-approvals: 1
|
||||
issue-title: "Approve deployment"
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Blue-Green**: Use for zero-downtime deployments
|
||||
2. **Canary**: Use for gradual rollouts with monitoring
|
||||
3. **Automated Rollback**: Always have rollback plan
|
||||
4. **Smoke Tests**: Run immediately after deployment
|
||||
5. **Health Checks**: Monitor health continuously
|
||||
6. **Gates**: Use deployment gates for critical deployments
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
1. **No Rollback Plan**: Can't recover from failed deployment
|
||||
```yaml
|
||||
# ✅ Always have rollback command ready
|
||||
kubectl rollout undo deployment/service
|
||||
```
|
||||
|
||||
2. **Skipping Smoke Tests**: Catching issues too late
|
||||
```yaml
|
||||
# ✅ Run smoke tests immediately after deploy
|
||||
- name: Smoke Tests
|
||||
run: ./scripts/smoke-tests.sh
|
||||
```
|
||||
|
||||
3. **100% Traffic Switch**: All-or-nothing failures
|
||||
```yaml
|
||||
# ❌ BAD: Immediate full switch
|
||||
# ✅ GOOD: Gradual rollout (10% → 50% → 100%)
|
||||
```
|
||||
|
||||
4. **No Health Monitoring**: Missing deployment issues
|
||||
```yaml
|
||||
# ✅ Monitor health after deployment
|
||||
- name: Monitor Health
|
||||
run: kubectl rollout status deployment/service --timeout=5m
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Strategy | Risk | Downtime | Resource Cost |
|
||||
|----------|------|----------|---------------|
|
||||
| **Blue-Green** | Low | Zero | 2x (temporary) |
|
||||
| **Canary** | Low | Zero | +10-20% |
|
||||
| **Rolling** | Medium | Zero | 1x |
|
||||
| **Recreate** | High | Yes | 1x |
|
||||
|
||||
**Deployment Commands:**
|
||||
```bash
|
||||
# Apply deployment
|
||||
kubectl apply -f kubernetes/
|
||||
|
||||
# Check rollout status
|
||||
kubectl rollout status deployment/servi
|
||||
@@ -1,430 +0,0 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
# Bilingual Code Comments
|
||||
|
||||
Add comprehensive code comments in both Vietnamese and English to improve code readability for international and Vietnamese teams.
|
||||
|
||||
## When to Use
|
||||
|
||||
- Adding comments to new code
|
||||
- Documenting existing code
|
||||
- Creating JSDoc/TSDoc documentation
|
||||
- Writing function/class descriptions
|
||||
- Explaining complex logic
|
||||
- Adding inline comments
|
||||
|
||||
## Comment Format
|
||||
|
||||
### Single-line Comments
|
||||
|
||||
```typescript
|
||||
// EN: Initialize database connection
|
||||
// VI: Khởi tạo kết nối database
|
||||
const db = await createConnection();
|
||||
```
|
||||
|
||||
### Multi-line Comments
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* EN: Validates user credentials and returns JWT token
|
||||
* VI: Xác thực thông tin đăng nhập và trả về JWT token
|
||||
*
|
||||
* @param email - User email address / Địa chỉ email người dùng
|
||||
* @param password - User password / Mật khẩu người dùng
|
||||
* @returns JWT token / Mã JWT token
|
||||
* @throws AuthenticationError if credentials invalid / Lỗi xác thực nếu thông tin không hợp lệ
|
||||
*/
|
||||
async function login(email: string, password: string): Promise<string> {
|
||||
// Implementation
|
||||
}
|
||||
```
|
||||
|
||||
## Core Comment Patterns
|
||||
|
||||
### Function Documentation
|
||||
```typescript
|
||||
/**
|
||||
* EN: Calculates the total price including tax and discount
|
||||
* VI: Tính tổng giá bao gồm thuế và giảm giá
|
||||
*
|
||||
* @param basePrice - Original price / Giá gốc
|
||||
* @param taxRate - Tax rate (0-1) / Tỷ lệ thuế (0-1)
|
||||
* @param discount - Discount amount / Số tiền giảm giá
|
||||
* @returns Final price / Giá cuối cùng
|
||||
*/
|
||||
function calculateTotal(
|
||||
basePrice: number,
|
||||
taxRate: number,
|
||||
discount: number
|
||||
): number {
|
||||
// EN: Apply discount first
|
||||
// VI: Áp dụng giảm giá trước
|
||||
const discountedPrice = basePrice - discount;
|
||||
|
||||
// EN: Then calculate tax
|
||||
// VI: Sau đó tính thuế
|
||||
const tax = discountedPrice * taxRate;
|
||||
|
||||
return discountedPrice + tax;
|
||||
}
|
||||
```
|
||||
|
||||
### Class Documentation
|
||||
```typescript
|
||||
/**
|
||||
* EN: Handles user authentication and authorization
|
||||
* VI: Xử lý xác thực và phân quyền người dùng
|
||||
*/
|
||||
export class AuthService {
|
||||
/**
|
||||
* EN: JWT secret key from environment
|
||||
* VI: Khóa bí mật JWT từ biến môi trường
|
||||
*/
|
||||
private readonly jwtSecret: string;
|
||||
|
||||
/**
|
||||
* EN: Verify JWT token and return user payload
|
||||
* VI: Xác minh JWT token và trả về thông tin người dùng
|
||||
*
|
||||
* @param token - JWT token to verify / JWT token cần xác minh
|
||||
* @returns User payload / Thông tin người dùng
|
||||
* @throws TokenExpiredError if token expired / Lỗi token hết hạn
|
||||
*/
|
||||
async verifyToken(token: string): Promise<UserPayload> {
|
||||
// Implementation
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Interface/Type Documentation
|
||||
```typescript
|
||||
/**
|
||||
* EN: User data transfer object
|
||||
* VI: Đối tượng truyền dữ liệu người dùng
|
||||
*/
|
||||
interface UserDto {
|
||||
/** EN: Unique user identifier / VI: Mã định danh duy nhất */
|
||||
id: string;
|
||||
|
||||
/** EN: User email address / VI: Địa chỉ email người dùng */
|
||||
email: string;
|
||||
|
||||
/** EN: User display name / VI: Tên hiển thị người dùng */
|
||||
name: string;
|
||||
|
||||
/** EN: User role for authorization / VI: Vai trò người dùng để phân quyền */
|
||||
role: 'admin' | 'user' | 'guest';
|
||||
}
|
||||
```
|
||||
|
||||
### React Components
|
||||
```typescript
|
||||
/**
|
||||
* EN: User profile card component
|
||||
* VI: Component thẻ hồ sơ người dùng
|
||||
*
|
||||
* @param user - User data to display / Dữ liệu người dùng để hiển thị
|
||||
* @param onEdit - Callback when edit button clicked / Callback khi nhấn nút chỉnh sửa
|
||||
*/
|
||||
export function UserCard({ user, onEdit }: UserCardProps) {
|
||||
// EN: Local state for loading status
|
||||
// VI: State cục bộ cho trạng thái loading
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="user-card">
|
||||
{/* EN: Display user avatar / VI: Hiển thị avatar người dùng */}
|
||||
<img src={user.avatar} alt={user.name} />
|
||||
|
||||
{/* EN: User information section / VI: Phần thông tin người dùng */}
|
||||
<div className="user-info">
|
||||
<h3>{user.name}</h3>
|
||||
<p>{user.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Prisma Schema
|
||||
```prisma
|
||||
/// EN: User model for authentication and profile
|
||||
/// VI: Model người dùng cho xác thực và hồ sơ
|
||||
model User {
|
||||
/// EN: Unique identifier / VI: Mã định danh duy nhất
|
||||
id String @id @default(cuid())
|
||||
|
||||
/// EN: User email (unique) / VI: Email người dùng (duy nhất)
|
||||
email String @unique
|
||||
|
||||
/// EN: Hashed password / VI: Mật khẩu đã mã hóa
|
||||
password String
|
||||
|
||||
/// EN: Display name / VI: Tên hiển thị
|
||||
name String
|
||||
|
||||
/// EN: Account creation timestamp / VI: Thời gian tạo tài khoản
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
/// EN: Last update timestamp / VI: Thời gian cập nhật cuối
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@map("users")
|
||||
}
|
||||
```
|
||||
|
||||
### API Controllers
|
||||
```typescript
|
||||
/**
|
||||
* EN: User management controller
|
||||
* VI: Controller quản lý người dùng
|
||||
*/
|
||||
export class UserController {
|
||||
/**
|
||||
* EN: Get user by ID
|
||||
* VI: Lấy thông tin người dùng theo ID
|
||||
*
|
||||
* GET /api/users/:id
|
||||
*/
|
||||
async getById(req: Request, res: Response) {
|
||||
try {
|
||||
// EN: Extract user ID from params
|
||||
// VI: Lấy ID người dùng từ params
|
||||
const { id } = req.params;
|
||||
|
||||
// EN: Fetch user from database
|
||||
// VI: Lấy người dùng từ database
|
||||
const user = await this.userService.findById(id);
|
||||
|
||||
if (!user) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: {
|
||||
code: 'USER_NOT_FOUND',
|
||||
message: 'User not found / Không tìm thấy người dùng',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
data: user,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to get user', { error, userId: req.params.id });
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
error: {
|
||||
code: 'INTERNAL_ERROR',
|
||||
message: 'Internal server error / Lỗi máy chủ nội bộ',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Middleware
|
||||
```typescript
|
||||
/**
|
||||
* EN: Authentication middleware to verify JWT tokens
|
||||
* VI: Middleware xác thực để kiểm tra JWT token
|
||||
*/
|
||||
export function authMiddleware(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
// EN: Extract token from Authorization header
|
||||
// VI: Lấy token từ header Authorization
|
||||
const authHeader = req.headers.authorization;
|
||||
const token = authHeader?.replace('Bearer ', '');
|
||||
|
||||
if (!token) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
error: {
|
||||
code: 'NO_TOKEN',
|
||||
message: 'Authentication required / Yêu cầu xác thực',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
// EN: Verify token and extract payload
|
||||
// VI: Xác minh token và lấy payload
|
||||
const payload = jwt.verify(token, JWT_SECRET);
|
||||
req.user = payload;
|
||||
next();
|
||||
} catch (error) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
error: {
|
||||
code: 'INVALID_TOKEN',
|
||||
message: 'Invalid or expired token / Token không hợp lệ hoặc hết hạn',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Comment Placement
|
||||
- Place bilingual comments together (EN first, then VI)
|
||||
- Keep comments close to the code they describe
|
||||
- Use JSDoc format for functions and classes
|
||||
|
||||
### 2. Comment Content
|
||||
- **DO**: Explain WHY, not WHAT (code shows what)
|
||||
- **DO**: Document complex logic and business rules
|
||||
- **DO**: Include parameter descriptions and return types
|
||||
- **DO**: Document error conditions and exceptions
|
||||
- **DON'T**: State the obvious
|
||||
- **DON'T**: Write redundant comments
|
||||
|
||||
### 3. Language Guidelines
|
||||
- **English**: Use clear, concise technical English
|
||||
- **Vietnamese**: Use proper Vietnamese technical terms
|
||||
- Keep translations accurate and natural
|
||||
- Use consistent terminology across codebase
|
||||
|
||||
### 4. Special Cases
|
||||
|
||||
#### TODO Comments
|
||||
```typescript
|
||||
// TODO EN: Implement caching for better performance
|
||||
// TODO VI: Triển khai caching để cải thiện hiệu suất
|
||||
```
|
||||
|
||||
#### FIXME Comments
|
||||
```typescript
|
||||
// FIXME EN: This causes memory leak, needs refactoring
|
||||
// FIXME VI: Đoạn này gây rò rỉ bộ nhớ, cần refactor
|
||||
```
|
||||
|
||||
#### WARNING Comments
|
||||
```typescript
|
||||
// WARNING EN: Do not modify this without updating the database schema
|
||||
// WARNING VI: Không sửa đổi phần này mà không cập nhật schema database
|
||||
```
|
||||
|
||||
### 5. Documentation Priority
|
||||
|
||||
**High Priority** (Always document):
|
||||
- Public APIs and exported functions
|
||||
- Complex algorithms and business logic
|
||||
- Security-critical code
|
||||
- Configuration and environment setup
|
||||
- Error handling strategies
|
||||
|
||||
**Medium Priority** (Document when helpful):
|
||||
- Helper functions with non-obvious behavior
|
||||
- Data transformations
|
||||
- Integration points with external services
|
||||
|
||||
**Low Priority** (Optional):
|
||||
- Simple getters/setters
|
||||
- Self-explanatory code
|
||||
- Standard CRUD operations
|
||||
|
||||
|
||||
## Integration with Project Rules
|
||||
|
||||
When commenting code in this project:
|
||||
- Follow the code organization from `project-rules` skill
|
||||
- Use consistent terminology with project documentation
|
||||
- Align with the API response format standards
|
||||
- Document according to the testing standards
|
||||
- Include security considerations where relevant
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
1. **Stating the Obvious**: Comments that repeat what code says
|
||||
```typescript
|
||||
// ❌ BAD: States the obvious
|
||||
// EN: Set x to 5
|
||||
// VI: Gán x bằng 5
|
||||
const x = 5;
|
||||
|
||||
// ✅ GOOD: Explains why
|
||||
// EN: Default timeout in seconds for API calls
|
||||
// VI: Timeout mặc định (giây) cho các API call
|
||||
const x = 5;
|
||||
```
|
||||
|
||||
2. **Missing Translation**: Incomplete bilingual comments
|
||||
```typescript
|
||||
// ❌ BAD: Missing Vietnamese
|
||||
// EN: Calculate discount
|
||||
const discount = price * 0.1;
|
||||
|
||||
// ✅ GOOD: Both languages
|
||||
// EN: Calculate 10% discount for members
|
||||
// VI: Tính giảm giá 10% cho thành viên
|
||||
const discount = price * 0.1;
|
||||
```
|
||||
|
||||
3. **Outdated Comments**: Comments not matching code
|
||||
```typescript
|
||||
// ❌ BAD: Comment says email, code uses phone
|
||||
// EN: Validate email format
|
||||
// VI: Xác thực định dạng email
|
||||
validatePhone(phone);
|
||||
|
||||
// ✅ GOOD: Keep comments in sync with code
|
||||
// EN: Validate phone number format
|
||||
// VI: Xác thực định dạng số điện thoại
|
||||
validatePhone(phone);
|
||||
```
|
||||
|
||||
4. **No JSDoc for Public APIs**: Missing documentation for exports
|
||||
```typescript
|
||||
// ❌ BAD: Exported function without JSDoc
|
||||
export function processOrder(order: Order) { ... }
|
||||
|
||||
// ✅ GOOD: Full JSDoc for exported functions
|
||||
/**
|
||||
* EN: Process order and update inventory
|
||||
* VI: Xử lý đơn hàng và cập nhật kho
|
||||
* @param order - Order to process / Đơn hàng cần xử lý
|
||||
* @returns ProcessingResult / Kết quả xử lý
|
||||
*/
|
||||
export function processOrder(order: Order): ProcessingResult { ... }
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Function Comment Template
|
||||
```typescript
|
||||
/**
|
||||
* EN: [Brief description in English]
|
||||
* VI: [Mô tả ngắn gọn bằng tiếng Việt]
|
||||
*
|
||||
* @param paramName - EN description / VI mô tả
|
||||
* @returns EN description / VI mô tả
|
||||
* @throws ErrorType EN when / VI khi nào
|
||||
*/
|
||||
```
|
||||
|
||||
### Inline Comment Template
|
||||
```typescript
|
||||
// EN: [English explanation]
|
||||
// VI: [Giải thích tiếng Việt]
|
||||
```
|
||||
|
||||
### Complex Block Template
|
||||
```typescript
|
||||
// EN: Step N: [What this block does]
|
||||
// VI: Bước N: [Block này làm gì]
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
- [Project Rules](../project-rules/SKILL.md) - Code organization standards
|
||||
- [Documentation](../documentation/SKILL.md) - Documentation patterns
|
||||
- [API Design](../api-design/SKILL.md) - API documentation standards
|
||||
- [Testing Patterns](../testing-patterns/SKILL.md) - Test documentation
|
||||
@@ -1,35 +0,0 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
# Configuration Management Patterns
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use this skill when:
|
||||
- Implementing feature flags and feature toggles
|
||||
- Managing environment-specific configurations
|
||||
- Implementing dynamic configuration reloading
|
||||
- Managing secrets and sensitive configuration
|
||||
- Implementing configuration validation
|
||||
- Handling configuration versioning
|
||||
- Building configuration services
|
||||
- Implementing A/B testing with feature flags
|
||||
- Managing configuration across multiple environments
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Configuration Types
|
||||
|
||||
1. **Static Configuration**: Environment variables, config files
|
||||
2. **Dynamic Configuration**: Feature flags, runtime configs
|
||||
3. **Secrets**: Sensitive data (API keys, passwords)
|
||||
4. **Feature Flags**: Runtime feature toggles
|
||||
|
||||
### Configuration Sources
|
||||
|
||||
- Environment variables
|
||||
- Configuration files (JSON, YAML)
|
||||
- Configuration service (external)
|
||||
- Feature flag service
|
||||
- Secrets manager (Kubernetes Secrets, Vault)
|
||||
@@ -1,269 +0,0 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
# Data Consistency Patterns
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use this skill when:
|
||||
- Implementing distributed transactions across multiple services
|
||||
- Handling eventual consistency in microservices
|
||||
- Implementing Saga patterns for distributed workflows
|
||||
- Designing compensation strategies for failed transactions
|
||||
- Implementing idempotent operations
|
||||
- Managing data synchronization across services
|
||||
- Handling conflict resolution
|
||||
- Implementing optimistic locking strategies
|
||||
- Building event sourcing systems
|
||||
- Ensuring data integrity in distributed systems
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### ACID vs BASE
|
||||
|
||||
**ACID (Traditional Databases):**
|
||||
- Atomicity: All or nothing
|
||||
- Consistency: Data always valid
|
||||
- Isolation: Concurrent transactions isolated
|
||||
- Durability: Committed changes persist
|
||||
|
||||
**BASE (Distributed Systems):**
|
||||
- Basic Availability: System available most of the time
|
||||
- Soft state: State may change over time
|
||||
- Eventual consistency: Consistency achieved eventually
|
||||
|
||||
### Consistency Models
|
||||
|
||||
| Model | Description | Use Case |
|
||||
|-------|-------------|----------|
|
||||
| **Strong** | All nodes see same data at same time | Financial transactions |
|
||||
| **Weak** | No guarantees about when consistency occurs | Caching |
|
||||
| **Eventual** | System becomes consistent over time | Read models, analytics |
|
||||
| **Causal** | Related operations maintain order | User sessions |
|
||||
|
||||
### Distributed Transaction Challenges
|
||||
|
||||
- Network partitions
|
||||
- Service failures
|
||||
- Clock synchronization
|
||||
- Partial failures
|
||||
- Two-Phase Commit (2PC) limitations
|
||||
|
||||
## Saga Pattern
|
||||
|
||||
### Orchestration vs Choreography
|
||||
|
||||
| Approach | Central Control | Resilience | Complexity | Best For |
|
||||
|----------|-----------------|------------|------------|----------|
|
||||
| **Orchestration** | Yes (single coordinator) | Lower (SPOF) | Easier to debug | Complex workflows |
|
||||
| **Choreography** | No (event-driven) | Higher | Harder to trace | Simple flows, loose coupling |
|
||||
|
||||
### Saga Execution Flow
|
||||
|
||||
```
|
||||
Execute: Step1 -> Step2 -> Step3 -> Complete
|
||||
| | |
|
||||
v v v
|
||||
Compensate: <- Step2.undo <- Step1.undo (on failure)
|
||||
```
|
||||
|
||||
### Key Saga Interfaces
|
||||
|
||||
```typescript
|
||||
interface SagaStep {
|
||||
name: string;
|
||||
execute: () => Promise<any>;
|
||||
compensate: (context: any) => Promise<void>;
|
||||
retry?: number;
|
||||
}
|
||||
|
||||
interface SagaContext {
|
||||
sagaId: string;
|
||||
steps: SagaStep[];
|
||||
currentStep: number;
|
||||
data: Record<string, any>;
|
||||
status: 'pending' | 'running' | 'completed' | 'compensating' | 'failed';
|
||||
}
|
||||
```
|
||||
|
||||
See [./references/REFERENCE.md](./references/REFERENCE.md) for full Saga orchestrator implementation.
|
||||
|
||||
## Idempotency Pattern
|
||||
|
||||
Ensures operations can be safely retried without side effects.
|
||||
|
||||
### Key Concepts
|
||||
|
||||
- **Idempotency Key**: Unique identifier for each operation
|
||||
- **Result Storage**: Cache results to return on duplicates
|
||||
- **TTL**: Time-to-live for idempotency records
|
||||
|
||||
### Pattern
|
||||
|
||||
```typescript
|
||||
const key = `${operation}:${userId}:${hash(requestData)}`;
|
||||
await idempotencyHandler.execute(key, () => operation());
|
||||
```
|
||||
|
||||
See [./references/REFERENCE.md](./references/REFERENCE.md) for full implementation.
|
||||
|
||||
## Optimistic Locking
|
||||
|
||||
Prevents lost updates in concurrent scenarios using version fields.
|
||||
|
||||
### Pattern
|
||||
|
||||
```typescript
|
||||
await prisma.entity.update({
|
||||
where: { id, version: currentVersion },
|
||||
data: { ...updates, version: { increment: 1 } }
|
||||
});
|
||||
```
|
||||
|
||||
### Prisma Schema
|
||||
|
||||
```prisma
|
||||
model Entity {
|
||||
id String @id @default(cuid())
|
||||
version Int @default(1)
|
||||
// ... other fields
|
||||
}
|
||||
```
|
||||
|
||||
See [./references/REFERENCE.md](./references/REFERENCE.md) for full optimistic locking service.
|
||||
|
||||
## CQRS Pattern
|
||||
|
||||
Command Query Responsibility Segregation separates read and write operations.
|
||||
|
||||
### Architecture
|
||||
|
||||
```
|
||||
Write Path: Command -> Write Model -> Event -> Event Store
|
||||
Read Path: Query -> Read Model (denormalized, optimized for reads)
|
||||
```
|
||||
|
||||
### Key Benefits
|
||||
|
||||
- Optimized read models for query performance
|
||||
- Independent scaling of read/write operations
|
||||
- Eventual consistency between models
|
||||
|
||||
See [./references/REFERENCE.md](./references/REFERENCE.md) for implementation details.
|
||||
|
||||
## Conflict Resolution Strategies
|
||||
|
||||
| Strategy | Description | Use Case |
|
||||
|----------|-------------|----------|
|
||||
| **Last Write Wins** | Latest timestamp wins | Simple scenarios |
|
||||
| **First Write Wins** | Earliest timestamp wins | Preserving original data |
|
||||
| **Merge** | Combine both versions | Non-conflicting fields |
|
||||
| **Manual** | Store for human review | Critical data conflicts |
|
||||
|
||||
## Outbox Pattern
|
||||
|
||||
Ensures reliable event publishing by storing events in the same transaction as business data.
|
||||
|
||||
### Flow
|
||||
|
||||
1. Execute business operation in transaction
|
||||
2. Store event in outbox table (same transaction)
|
||||
3. Background processor publishes and marks as sent
|
||||
|
||||
See [./references/REFERENCE.md](./references/REFERENCE.md) for implementation.
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Saga Pattern
|
||||
- Design compensations for every step
|
||||
- Make steps idempotent for retries
|
||||
- Set timeouts for saga execution
|
||||
- Monitor saga execution and compensation
|
||||
- Choose orchestration for complex workflows, choreography for simple ones
|
||||
|
||||
### Idempotency
|
||||
- Generate keys from request data
|
||||
- Store keys with results
|
||||
- Set appropriate TTL for records
|
||||
- Regularly clean expired records
|
||||
|
||||
### Eventual Consistency
|
||||
- Accept that consistency is eventual
|
||||
- Use separate read models for queries
|
||||
- Define resolution strategies upfront
|
||||
- Monitor consistency lag
|
||||
|
||||
### Optimistic Locking
|
||||
- Add version fields to entities
|
||||
- Implement retry with exponential backoff
|
||||
- Handle conflicts gracefully
|
||||
- Use for read-heavy workloads
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
### No Compensation Logic
|
||||
```typescript
|
||||
// BAD: No compensation
|
||||
steps: [{ execute: () => createOrder() }]
|
||||
|
||||
// GOOD: Always define compensation
|
||||
steps: [{
|
||||
execute: () => createOrder(),
|
||||
compensate: (ctx) => cancelOrder(ctx.orderId)
|
||||
}]
|
||||
```
|
||||
|
||||
### Missing Idempotency
|
||||
```typescript
|
||||
// BAD: Creates duplicate on retry
|
||||
await createPayment(orderId);
|
||||
|
||||
// GOOD: Idempotent check
|
||||
const existing = await findPayment(idempotencyKey);
|
||||
if (existing) return existing;
|
||||
await createPayment(orderId);
|
||||
```
|
||||
|
||||
### Ignoring Partial Failures
|
||||
```typescript
|
||||
// BAD: No error handling
|
||||
await step1(); await step2(); await step3();
|
||||
|
||||
// GOOD: Saga orchestration
|
||||
await sagaOrchestrator.execute(context);
|
||||
```
|
||||
|
||||
### No Version Field
|
||||
```prisma
|
||||
// GOOD: Add version for optimistic locking
|
||||
model Entity {
|
||||
version Int @default(1)
|
||||
}
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Pattern | Use Case | Complexity |
|
||||
|---------|----------|------------|
|
||||
| **Saga (Orchestrated)** | Complex multi-step workflows | High |
|
||||
| **Saga (Choreography)** | Simple event-driven flows | Medium |
|
||||
| **Outbox Pattern** | Guaranteed event publishing | Medium |
|
||||
| **Idempotency** | Retry-safe operations | Low |
|
||||
| **Optimistic Lock** | Concurrent updates | Low |
|
||||
| **CQRS** | Read/write optimization | High |
|
||||
| **Dead Letter Queue** | Failed message handling | Medium |
|
||||
|
||||
## Resources
|
||||
|
||||
### Internal References
|
||||
- [Detailed Code Examples](./references/REFERENCE.md) - Full implementations for all patterns
|
||||
- [Event-Driven Architecture](../event-driven-architecture/SKILL.md) - Event patterns
|
||||
- [Error Handling Patterns](../error-handling-patterns/SKILL.md) - Error handling
|
||||
- [Database & Prisma](../database-prisma/SKILL.md) - Database patterns
|
||||
- [Project Rules](../project-rules/SKILL.md) - GoodGo coding standards
|
||||
|
||||
### External Resources
|
||||
- [Saga Pattern](https://microservices.io/patterns/data/saga.html) - Saga pattern overview
|
||||
- [Event Sourcing](https://martinfowler.com/eaaDev/EventSourcing.html) - Event sourcing pattern
|
||||
- [CQRS Pattern](https://martinfowler.com/bliki/CQRS.html) - CQRS pattern
|
||||
@@ -1,184 +0,0 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
# Prisma Database Patterns
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use this skill when:
|
||||
- Setting up Prisma for a new service
|
||||
- Creating or modifying database schemas
|
||||
- Writing database migrations
|
||||
- Implementing repository patterns
|
||||
- Optimizing database queries
|
||||
- Setting up database connections
|
||||
- Implementing transactions
|
||||
- Working with Neon PostgreSQL
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Architecture
|
||||
- Repository pattern for data access
|
||||
- Prisma as ORM for type safety
|
||||
- Neon PostgreSQL as primary database
|
||||
- Connection pooling for performance
|
||||
- Transaction support for data consistency
|
||||
|
||||
## Key Patterns
|
||||
|
||||
### Prisma Setup
|
||||
|
||||
```bash
|
||||
npm install @prisma/client prisma
|
||||
npx prisma init
|
||||
```
|
||||
|
||||
### Schema Definition
|
||||
|
||||
```prisma
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
email String @unique
|
||||
name String?
|
||||
role Role @default(USER)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
posts Post[]
|
||||
|
||||
@@index([email])
|
||||
@@index([createdAt])
|
||||
@@map("users")
|
||||
}
|
||||
```
|
||||
|
||||
### Database Connection
|
||||
|
||||
```typescript
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const globalForPrisma = global as unknown as { prisma: PrismaClient | undefined };
|
||||
|
||||
export const prisma = globalForPrisma.prisma ?? new PrismaClient({
|
||||
log: process.env.NODE_ENV === 'development' ? ['query', 'error'] : ['error'],
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;
|
||||
```
|
||||
|
||||
### Repository Pattern
|
||||
|
||||
```typescript
|
||||
export class UserRepository {
|
||||
async findById(id: string) {
|
||||
return prisma.user.findUnique({ where: { id }, include: { profile: true } });
|
||||
}
|
||||
|
||||
async findAll({ page = 1, limit = 10, search }: QueryOptions) {
|
||||
const where = search ? { OR: [
|
||||
{ email: { contains: search, mode: 'insensitive' } },
|
||||
{ name: { contains: search, mode: 'insensitive' } }
|
||||
]} : {};
|
||||
|
||||
const [data, total] = await Promise.all([
|
||||
prisma.user.findMany({ where, skip: (page - 1) * limit, take: limit }),
|
||||
prisma.user.count({ where })
|
||||
]);
|
||||
return { data, total };
|
||||
}
|
||||
|
||||
async create(data: CreateUserDto) {
|
||||
return prisma.user.create({ data });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Transactions
|
||||
|
||||
```typescript
|
||||
await prisma.$transaction(async (tx) => {
|
||||
await tx.account.update({ where: { id: from }, data: { balance: { decrement: amount } } });
|
||||
await tx.account.update({ where: { id: to }, data: { balance: { increment: amount } } });
|
||||
}, { maxWait: 5000, timeout: 10000 });
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
- **Schema Design**: Use appropriate field types, add indexes for frequently queried fields
|
||||
- **Performance**: Use select to fetch only needed fields, implement pagination
|
||||
- **Security**: Never expose sensitive fields, use parameterized queries
|
||||
- **Maintenance**: Keep migrations small and focused, test before production
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
1. **N+1 Query Problem**: Fetching related data in a loop
|
||||
```typescript
|
||||
// BAD: N+1 queries
|
||||
for (const user of users) { await prisma.post.findMany({ where: { authorId: user.id } }); }
|
||||
// GOOD: Include relations
|
||||
await prisma.user.findMany({ include: { posts: true } });
|
||||
```
|
||||
|
||||
2. **No Indexes**: Missing indexes on frequently queried columns
|
||||
```prisma
|
||||
// GOOD: Add index
|
||||
@@index([createdAt])
|
||||
```
|
||||
|
||||
3. **Raw Queries Without Parameters**: SQL injection risk
|
||||
```typescript
|
||||
// BAD: prisma.$queryRawUnsafe(`SELECT * FROM users WHERE email = '${email}'`);
|
||||
// GOOD: prisma.$queryRaw`SELECT * FROM users WHERE email = ${email}`;
|
||||
```
|
||||
|
||||
4. **Not Using Transactions**: Data inconsistency risk
|
||||
```typescript
|
||||
// GOOD: Use transaction for related operations
|
||||
await prisma.$transaction([update1, update2]);
|
||||
```
|
||||
|
||||
5. **Exposing Internal IDs**: Leaking database structure
|
||||
```prisma
|
||||
// GOOD: Use CUID or UUID
|
||||
model User { id String @id @default(cuid()) }
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Operation | Command |
|
||||
|-----------|---------|
|
||||
| **Create migration** | `npx prisma migrate dev --name <name>` |
|
||||
| **Apply migrations** | `npx prisma migrate deploy` |
|
||||
| **Reset database** | `npx prisma migrate reset` |
|
||||
| **Generate client** | `npx prisma generate` |
|
||||
| **Open Studio** | `npx prisma studio` |
|
||||
| **Seed database** | `npx prisma db seed` |
|
||||
|
||||
**Common Query Patterns:**
|
||||
```typescript
|
||||
await prisma.user.findUnique({ where: { id } }); // Find unique
|
||||
await prisma.user.findMany({ include: { posts: true } }); // With relations
|
||||
await prisma.user.findMany({ select: { id: true } }); // Select fields
|
||||
await prisma.user.findMany({ skip: 10, take: 10 }); // Pagination
|
||||
await prisma.$transaction(async (tx) => { ... }); // Transaction
|
||||
```
|
||||
|
||||
**Schema Field Types:**
|
||||
| Type | PostgreSQL | Description |
|
||||
|------|------------|-------------|
|
||||
| `String` | TEXT | Variable-length string |
|
||||
| `Int` | INTEGER | 32-bit integer |
|
||||
| `BigInt` | BIGINT | 64-bit integer |
|
||||
| `Float` | DOUBLE PRECISION | Floating point |
|
||||
| `Boolean` | BOOLEAN | True/false |
|
||||
| `DateTime` | TIMESTAMP | Date and time |
|
||||
| `Json` | JSONB | JSON data |
|
||||
|
||||
## Resources
|
||||
|
||||
- [Prisma Documentation](https://www.prisma.io/docs) - Official Prisma docs
|
||||
- [Prisma Schema Reference](https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference)
|
||||
- [Detailed Code Examples](./references/REFERENCE.md)
|
||||
- [Repository Pattern](../repository-pattern/SKILL.md) - Data access patterns
|
||||
- [Caching Patterns](../caching-patterns/SKILL.md) - Query caching
|
||||
- [Testing Patterns](../testing-patterns/SKILL.md) - Database mocking
|
||||
@@ -1,183 +0,0 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
# Kubernetes Deployment Patterns
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use this skill when:
|
||||
- Deploying services to staging/production environments
|
||||
- Creating or updating Kubernetes manifests
|
||||
- Configuring autoscaling (HPA/VPA)
|
||||
- Setting up ingress and load balancing
|
||||
- Managing secrets and configmaps
|
||||
- Troubleshooting deployment issues
|
||||
- Implementing health checks and probes
|
||||
- Setting up monitoring and logging
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Deployment Strategy
|
||||
- Rolling updates for zero-downtime deployments
|
||||
- Resource limits and requests for stability
|
||||
- Health checks (liveness/readiness probes)
|
||||
- Horizontal Pod Autoscaler (HPA) for auto-scaling
|
||||
- ConfigMaps for configuration, Secrets for sensitive data
|
||||
|
||||
## Key Patterns
|
||||
|
||||
### Deployment Manifest
|
||||
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: auth-service
|
||||
namespace: goodgo
|
||||
spec:
|
||||
replicas: 3
|
||||
selector:
|
||||
matchLabels:
|
||||
app: auth-service
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: auth-service
|
||||
image: goodgo/auth-service:v1.0.0
|
||||
resources:
|
||||
requests: { memory: "256Mi", cpu: "250m" }
|
||||
limits: { memory: "512Mi", cpu: "500m" }
|
||||
livenessProbe:
|
||||
httpGet: { path: /health, port: 3000 }
|
||||
initialDelaySeconds: 30
|
||||
readinessProbe:
|
||||
httpGet: { path: /ready, port: 3000 }
|
||||
initialDelaySeconds: 5
|
||||
env:
|
||||
- name: DATABASE_URL
|
||||
valueFrom:
|
||||
secretKeyRef: { name: db-secrets, key: url }
|
||||
```
|
||||
|
||||
### HPA Configuration
|
||||
|
||||
```yaml
|
||||
apiVersion: autoscaling/v2
|
||||
kind: HorizontalPodAutoscaler
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: auth-service
|
||||
minReplicas: 2
|
||||
maxReplicas: 10
|
||||
metrics:
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
target: { type: Utilization, averageUtilization: 70 }
|
||||
```
|
||||
|
||||
### Ingress
|
||||
|
||||
```yaml
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
spec:
|
||||
tls:
|
||||
- hosts: [api.goodgo.com]
|
||||
secretName: api-tls-secret
|
||||
rules:
|
||||
- host: api.goodgo.com
|
||||
http:
|
||||
paths:
|
||||
- path: /auth
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service: { name: auth-service, port: { number: 80 } }
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
- **Resource Management**: Always set resource requests and limits, use HPA for scaling
|
||||
- **Configuration**: Use ConfigMaps for config, Secrets for sensitive data
|
||||
- **Health Checks**: Implement both liveness and readiness probes
|
||||
- **Deployment**: Use rolling updates, set maxSurge/maxUnavailable appropriately
|
||||
- **Security**: Run as non-root, use network policies, update base images regularly
|
||||
- **Monitoring**: Expose metrics endpoint, set up alerts
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
1. **No Resource Limits**: Pods consuming all node resources
|
||||
```yaml
|
||||
# GOOD: Set limits
|
||||
resources:
|
||||
requests: { memory: "256Mi", cpu: "250m" }
|
||||
limits: { memory: "512Mi", cpu: "500m" }
|
||||
```
|
||||
|
||||
2. **Missing Health Checks**: K8s can't detect unhealthy pods
|
||||
```yaml
|
||||
# GOOD: Add probes
|
||||
livenessProbe:
|
||||
httpGet: { path: /health, port: 3000 }
|
||||
readinessProbe:
|
||||
httpGet: { path: /ready, port: 3000 }
|
||||
```
|
||||
|
||||
3. **Hardcoded Secrets**: Exposing sensitive data
|
||||
```yaml
|
||||
# BAD: value: "secret123"
|
||||
# GOOD: valueFrom: secretKeyRef: { name: secrets, key: password }
|
||||
```
|
||||
|
||||
4. **Using `latest` Tag**: Unpredictable deployments
|
||||
```yaml
|
||||
# BAD: image: app:latest
|
||||
# GOOD: image: app:v1.2.3
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Resource | Command |
|
||||
|----------|---------|
|
||||
| **Apply manifests** | `kubectl apply -f kubernetes/` |
|
||||
| **Get pods** | `kubectl get pods -n goodgo` |
|
||||
| **Get logs** | `kubectl logs -f deployment/app -n goodgo` |
|
||||
| **Scale** | `kubectl scale deployment/app --replicas=5` |
|
||||
| **Rollback** | `kubectl rollout undo deployment/app` |
|
||||
| **Port forward** | `kubectl port-forward svc/app 3000:80` |
|
||||
| **Exec into pod** | `kubectl exec -it pod-name -- /bin/sh` |
|
||||
|
||||
**Resource Sizing Guidelines:**
|
||||
| Service Type | Memory Request | Memory Limit | CPU Request | CPU Limit |
|
||||
|--------------|----------------|--------------|-------------|-----------|
|
||||
| Microservice | 256Mi | 512Mi | 250m | 500m |
|
||||
| API Gateway | 512Mi | 1Gi | 500m | 1000m |
|
||||
| Database | 1Gi | 2Gi | 500m | 1000m |
|
||||
|
||||
**Health Check Defaults:**
|
||||
```yaml
|
||||
livenessProbe:
|
||||
initialDelaySeconds: 30 # Wait for app startup
|
||||
periodSeconds: 10 # Check every 10s
|
||||
failureThreshold: 3 # Restart after 3 failures
|
||||
|
||||
readinessProbe:
|
||||
initialDelaySeconds: 5 # Start checking early
|
||||
periodSeconds: 5 # Check frequently
|
||||
failureThreshold: 3 # Remove from LB after 3 failures
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
- [Kubernetes Documentation](https://kubernetes.io/docs/) - Official K8s docs
|
||||
- [Helm](https://helm.sh/docs/) - K8s package manager
|
||||
- [Detailed Manifests](./references/REFERENCE.md)
|
||||
- [Infrastructure as Code](../infrastructure-as-code/SKILL.md) - Terraform patterns
|
||||
- [Observability & Monitoring](../observability-monitoring/SKILL.md) - Health checks
|
||||
- [Service Discovery](../service-discovery-registry/SKILL.md) - K8s DNS patterns
|
||||
@@ -1,444 +0,0 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
# Documentation Writing Guidelines
|
||||
|
||||
## Documentation Structure
|
||||
|
||||
```
|
||||
docs/
|
||||
├── en/ # English documentation
|
||||
│ ├── guides/ # How-to guides
|
||||
│ │ ├── getting-started.md
|
||||
│ │ ├── development.md
|
||||
│ │ ├── deployment.md
|
||||
│ │ └── local-development.md
|
||||
│ ├── architecture/ # System design docs
|
||||
│ │ ├── system-design.md
|
||||
│ │ └── service-communication.md
|
||||
│ ├── api/ # API documentation
|
||||
│ │ └── openapi/
|
||||
│ └── runbooks/ # Operational guides
|
||||
│ ├── incident-response.md
|
||||
│ └── rollback-procedure.md
|
||||
├── vi/ # Vietnamese documentation (mirror structure)
|
||||
└── README.md # Documentation index
|
||||
```
|
||||
|
||||
## Where to Put Documentation
|
||||
|
||||
### Project-Level Documentation
|
||||
- **Location**: `docs/en/` and `docs/vi/`
|
||||
- **Examples**: Getting started, deployment guides, architecture
|
||||
- **Format**: Markdown with bilingual support
|
||||
|
||||
### Service/Package Documentation
|
||||
- **Location**: `services/[service-name]/README.md` or `packages/[package-name]/README.md`
|
||||
- **Content**: Service-specific setup, API endpoints, configuration
|
||||
- **Format**: Single README with bilingual sections
|
||||
|
||||
### Deployment Documentation
|
||||
- **Location**: `deployments/[environment]/README.md`
|
||||
- **Content**: Environment-specific deployment instructions
|
||||
- **Format**: Technical, operations-focused
|
||||
|
||||
### Infrastructure Documentation
|
||||
- **Location**: `infra/[component]/README.md`
|
||||
- **Content**: Infrastructure component configuration and usage
|
||||
- **Examples**: `infra/traefik/README.md`, `infra/observability/README.md`
|
||||
|
||||
## Bilingual Documentation Rules
|
||||
|
||||
### Format Options
|
||||
|
||||
**Option 1: Side-by-side (Recommended for short content)**
|
||||
```markdown
|
||||
# Service Name / Tên Dịch Vụ
|
||||
|
||||
This is a description.
|
||||
Đây là mô tả.
|
||||
```
|
||||
|
||||
**Option 2: Separate files (Recommended for long content)**
|
||||
```
|
||||
docs/
|
||||
├── en/
|
||||
│ └── guides/
|
||||
│ └── deployment.md
|
||||
└── vi/
|
||||
└── guides/
|
||||
└── deployment.md
|
||||
```
|
||||
|
||||
**Option 3: Sections (For mixed content)**
|
||||
```markdown
|
||||
# English Section
|
||||
|
||||
Content in English...
|
||||
|
||||
---
|
||||
|
||||
# Phần Tiếng Việt
|
||||
|
||||
Nội dung bằng tiếng Việt...
|
||||
```
|
||||
|
||||
### When to Use Each Format
|
||||
|
||||
- **Side-by-side**: README files, short guides, configuration docs
|
||||
- **Separate files**: Long guides (>200 lines), architecture docs, runbooks
|
||||
- **Sections**: API documentation, technical specifications
|
||||
|
||||
## Documentation Templates
|
||||
|
||||
### Service README Template
|
||||
|
||||
```markdown
|
||||
# Service Name / Tên Dịch Vụ
|
||||
|
||||
> **EN**: Brief description in English
|
||||
> **VI**: Mô tả ngắn gọn bằng tiếng Việt
|
||||
|
||||
## Features / Tính Năng
|
||||
|
||||
- Feature 1 / Tính năng 1
|
||||
- Feature 2 / Tính năng 2
|
||||
|
||||
## Prerequisites / Yêu Cầu
|
||||
|
||||
- Node.js 20+
|
||||
- PostgreSQL (Neon)
|
||||
- Redis
|
||||
|
||||
## Quick Start / Bắt Đầu Nhanh
|
||||
|
||||
```bash
|
||||
# Install dependencies / Cài đặt dependencies
|
||||
pnpm install
|
||||
|
||||
# Setup environment / Thiết lập môi trường
|
||||
cp .env.example .env
|
||||
|
||||
# Start service / Khởi động service
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
## Configuration / Cấu Hình
|
||||
|
||||
| Variable | Description / Mô Tả | Default | Required |
|
||||
|----------|---------------------|---------|----------|
|
||||
| PORT | Server port / Cổng server | 5000 | No |
|
||||
|
||||
## API Endpoints
|
||||
|
||||
See [API Documentation](../../docs/api/openapi/service-name.yaml)
|
||||
|
||||
## Development / Phát Triển
|
||||
|
||||
[Development instructions...]
|
||||
|
||||
## Testing / Kiểm Thử
|
||||
|
||||
```bash
|
||||
pnpm test
|
||||
```
|
||||
|
||||
## Deployment / Triển Khai
|
||||
|
||||
See [Deployment Guide](../../docs/en/guides/deployment.md)
|
||||
```
|
||||
|
||||
### Guide Template (docs/en/guides/)
|
||||
|
||||
```markdown
|
||||
# Guide Title
|
||||
|
||||
**Last Updated**: 2024-01-01
|
||||
**Difficulty**: Beginner/Intermediate/Advanced
|
||||
|
||||
## Overview
|
||||
|
||||
Brief overview of what this guide covers.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Requirement 1
|
||||
- Requirement 2
|
||||
|
||||
## Step-by-Step Instructions
|
||||
|
||||
### Step 1: Title
|
||||
|
||||
Description and commands...
|
||||
|
||||
```bash
|
||||
command here
|
||||
```
|
||||
|
||||
### Step 2: Title
|
||||
|
||||
Description and commands...
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Issue 1
|
||||
|
||||
**Problem**: Description
|
||||
**Solution**: Steps to fix
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Link to related guide
|
||||
- Link to another resource
|
||||
|
||||
## Resources
|
||||
|
||||
- [Related Doc](../path/to/doc.md)
|
||||
- [External Link](https://example.com)
|
||||
```
|
||||
|
||||
### Architecture Document Template
|
||||
|
||||
```markdown
|
||||
# Component Architecture
|
||||
|
||||
## Overview
|
||||
|
||||
High-level description of the component.
|
||||
|
||||
## Architecture Diagram
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[Component A] --> B[Component B]
|
||||
B --> C[Component C]
|
||||
```
|
||||
|
||||
## Components
|
||||
|
||||
### Component Name
|
||||
|
||||
**Purpose**: What it does
|
||||
**Technology**: Tech stack
|
||||
**Dependencies**: What it depends on
|
||||
|
||||
## Data Flow
|
||||
|
||||
1. Step 1
|
||||
2. Step 2
|
||||
3. Step 3
|
||||
|
||||
## Design Decisions
|
||||
|
||||
### Decision 1
|
||||
|
||||
**Context**: Why this decision was needed
|
||||
**Decision**: What was decided
|
||||
**Consequences**: Impact of the decision
|
||||
|
||||
## Deployment
|
||||
|
||||
How this component is deployed.
|
||||
|
||||
## Monitoring
|
||||
|
||||
How to monitor this component.
|
||||
```
|
||||
|
||||
## Writing Style Guidelines
|
||||
|
||||
### Technical Writing Principles
|
||||
|
||||
1. **Clear and Concise**: Use simple language, avoid jargon
|
||||
2. **Action-Oriented**: Start with verbs (Install, Configure, Deploy)
|
||||
3. **Structured**: Use headings, lists, and tables
|
||||
4. **Examples**: Provide code examples and commands
|
||||
5. **Visual**: Use diagrams where helpful
|
||||
|
||||
### Code Examples
|
||||
|
||||
```markdown
|
||||
# Good: With context and explanation
|
||||
Install dependencies using pnpm:
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
# Bad: No context
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
```
|
||||
|
||||
### Commands
|
||||
|
||||
- Always show the full command
|
||||
- Include comments for clarity
|
||||
- Show expected output when helpful
|
||||
|
||||
```bash
|
||||
# Good
|
||||
docker-compose up -d
|
||||
# Expected output: Creating network, Starting containers...
|
||||
|
||||
# Bad
|
||||
docker-compose up
|
||||
```
|
||||
|
||||
### Links
|
||||
|
||||
- Use relative links for internal docs
|
||||
- Use descriptive link text (not "click here")
|
||||
|
||||
```markdown
|
||||
# Good
|
||||
See the [Deployment Guide](../guides/deployment.md) for details.
|
||||
|
||||
# Bad
|
||||
Click [here](../guides/deployment.md) for more info.
|
||||
```
|
||||
|
||||
## Documentation Checklist
|
||||
|
||||
### Before Writing
|
||||
|
||||
- [ ] Determine correct location (docs/ vs service README)
|
||||
- [ ] Choose bilingual format (side-by-side vs separate)
|
||||
- [ ] Review existing docs for consistency
|
||||
|
||||
### While Writing
|
||||
|
||||
- [ ] Use clear, concise language
|
||||
- [ ] Include code examples
|
||||
- [ ] Add diagrams where helpful
|
||||
- [ ] Provide troubleshooting section
|
||||
- [ ] Link to related documentation
|
||||
|
||||
### After Writing
|
||||
|
||||
- [ ] Test all commands and code examples
|
||||
- [ ] Check all links work
|
||||
- [ ] Ensure bilingual consistency
|
||||
- [ ] Update documentation index (docs/README.md)
|
||||
- [ ] Request review from team
|
||||
|
||||
## Common Mistakes to Avoid
|
||||
|
||||
### ❌ Don't
|
||||
|
||||
- Write documentation in only one language
|
||||
- Put detailed guides in service README (use docs/)
|
||||
- Use absolute paths in links
|
||||
- Assume prior knowledge
|
||||
- Skip code examples
|
||||
- Forget to update when code changes
|
||||
|
||||
### ✅ Do
|
||||
|
||||
- Maintain bilingual documentation
|
||||
- Use appropriate location (docs/ vs README)
|
||||
- Use relative links
|
||||
- Explain prerequisites
|
||||
- Provide working examples
|
||||
- Keep docs up-to-date with code
|
||||
|
||||
## Documentation Maintenance
|
||||
|
||||
### When to Update Documentation
|
||||
|
||||
- New feature added
|
||||
- API changes
|
||||
- Configuration changes
|
||||
- Deployment process changes
|
||||
- Bug fixes affecting usage
|
||||
- Architecture changes
|
||||
|
||||
### Version Documentation
|
||||
|
||||
For major changes, consider:
|
||||
- Adding "Last Updated" date
|
||||
- Creating versioned docs (v1/, v2/)
|
||||
- Maintaining changelog
|
||||
|
||||
## Tools and Resources
|
||||
|
||||
### Markdown Tools
|
||||
|
||||
- **Mermaid**: For diagrams
|
||||
- **Tables Generator**: For complex tables
|
||||
- **Markdown Linter**: For consistency
|
||||
|
||||
### Documentation Testing
|
||||
|
||||
```bash
|
||||
# Check for broken links
|
||||
find docs -name "*.md" -exec markdown-link-check {} \;
|
||||
|
||||
# Lint markdown files
|
||||
markdownlint docs/**/*.md
|
||||
```
|
||||
|
||||
## Examples from Project
|
||||
|
||||
### Good Documentation Examples
|
||||
|
||||
- `docs/en/guides/getting-started.md` - Clear step-by-step guide
|
||||
- `services/_template/README.md` - Comprehensive service README
|
||||
- `deployments/local/README.md` - Operations-focused deployment guide
|
||||
|
||||
### Documentation Locations Reference
|
||||
|
||||
| Content Type | Location | Format |
|
||||
|--------------|----------|--------|
|
||||
| Getting Started | `docs/en/guides/getting-started.md` | Separate files |
|
||||
| Service Setup | `services/[name]/README.md` | Side-by-side |
|
||||
| Deployment | `docs/en/guides/deployment.md` | Separate files |
|
||||
| Architecture | `docs/en/architecture/` | Separate files |
|
||||
| API Specs | `docs/en/api/openapi/` | OpenAPI YAML |
|
||||
| Runbooks | `docs/en/runbooks/` | Separate files |
|
||||
| Infrastructure | `infra/[component]/README.md` | Side-by-side |
|
||||
| Environment Config | `deployments/[env]/README.md` | Technical only |
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### File Naming
|
||||
|
||||
- Use kebab-case: `getting-started.md`
|
||||
- Be descriptive: `local-development.md` not `dev.md`
|
||||
- Match EN and VI filenames
|
||||
|
||||
### Heading Levels
|
||||
|
||||
```markdown
|
||||
# H1: Document Title (only one per file)
|
||||
## H2: Major Sections
|
||||
### H3: Subsections
|
||||
#### H4: Details (use sparingly)
|
||||
```
|
||||
|
||||
### Bilingual Patterns
|
||||
|
||||
```markdown
|
||||
# Pattern 1: Inline
|
||||
Description / Mô tả
|
||||
|
||||
# Pattern 2: After slash
|
||||
PORT=5000 # Server port / Cổng server
|
||||
|
||||
# Pattern 3: Table
|
||||
| Variable | Description / Mô Tả |
|
||||
|
||||
# Pattern 4: Code comments
|
||||
# EN: Install dependencies
|
||||
# VI: Cài đặt dependencies
|
||||
pnpm install
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
- [Project Rules](../project-rules/SKILL.md) - Project structure and standards
|
||||
- [Comment Code](../comment-code/SKILL.md) - Code commenting standards
|
||||
- [API Design](../api-design/SKILL.md) - API documentation
|
||||
- [Testing Patterns](../testing-patterns/SKILL.md) - Test documentation
|
||||
@@ -1,363 +0,0 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
# Error Handling Patterns
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use this skill when:
|
||||
- Implementing error handling in services, controllers, or repositories
|
||||
- Creating custom error classes for specific error scenarios
|
||||
- Standardizing error responses across APIs
|
||||
- Handling exceptions from external services or database operations
|
||||
- Implementing error middleware and global error handlers
|
||||
- Debugging error scenarios and improving error messages
|
||||
- Distinguishing between operational and programming errors
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Error Types
|
||||
|
||||
1. **Operational Errors**: Expected errors that occur during normal operation
|
||||
- Examples: Validation errors, authentication failures, resource not found
|
||||
- Should be handled gracefully and return appropriate HTTP status codes
|
||||
- Safe to expose error details to clients (with caution)
|
||||
|
||||
2. **Programming Errors**: Unexpected errors due to bugs in code
|
||||
- Examples: Null pointer exceptions, type errors, logic bugs
|
||||
- Should be logged with full details for debugging
|
||||
- Should return generic error messages to clients (hide implementation details)
|
||||
|
||||
### Error Code System
|
||||
|
||||
The platform uses a centralized error code system (`ErrorCode` enum) that:
|
||||
- Provides unique identifiers for each error type
|
||||
- Maps to HTTP status codes consistently
|
||||
- Enables error tracking and analytics
|
||||
- Supports internationalization
|
||||
|
||||
Error codes follow the pattern: `{CATEGORY}_{NUMBER}`
|
||||
- `AUTH_001` - Authentication errors
|
||||
- `VALIDATION_001` - Validation errors
|
||||
- `RESOURCE_001` - Resource errors
|
||||
- `DB_001` - Database errors
|
||||
|
||||
## Patterns
|
||||
|
||||
### Base Error Class: HttpError
|
||||
|
||||
All custom errors extend the `HttpError` base class:
|
||||
|
||||
```typescript
|
||||
export class HttpError extends Error {
|
||||
public readonly statusCode: number;
|
||||
public readonly errorCode: string;
|
||||
public readonly isOperational: boolean;
|
||||
public readonly details?: any;
|
||||
|
||||
constructor(
|
||||
message: string,
|
||||
statusCode: number = 500,
|
||||
errorCode: string = 'INTERNAL_ERROR',
|
||||
isOperational: boolean = true,
|
||||
details?: any
|
||||
) {
|
||||
super(message);
|
||||
this.statusCode = statusCode;
|
||||
this.errorCode = errorCode;
|
||||
this.isOperational = isOperational;
|
||||
this.details = details;
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
}
|
||||
|
||||
toApiResponse() {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: this.errorCode,
|
||||
message: this.message,
|
||||
...(this.details && { details: this.details }),
|
||||
},
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Standard Error Classes
|
||||
|
||||
Use these predefined error classes for common scenarios:
|
||||
|
||||
**Resource Errors:**
|
||||
- `NotFoundError` - 404: Resource not found
|
||||
- `ConflictError` - 409: Resource conflict (e.g., duplicate)
|
||||
|
||||
**Validation Errors:**
|
||||
- `ValidationError` - 422: Input validation failed
|
||||
- `BadRequestError` - 400: Invalid request
|
||||
|
||||
**Authentication/Authorization:**
|
||||
- `UnauthorizedError` - 401: Authentication required
|
||||
- `ForbiddenError` - 403: Access denied
|
||||
|
||||
**System Errors:**
|
||||
- `InternalServerError` - 500: Internal server error (programming error)
|
||||
- `ServiceUnavailableError` - 503: Service temporarily unavailable
|
||||
- `DatabaseError` - 500: Database operation failed
|
||||
- `ExternalServiceError` - 502: External service error
|
||||
|
||||
**Rate Limiting:**
|
||||
- `RateLimitError` - 429: Too many requests
|
||||
|
||||
### Error Code Enum
|
||||
|
||||
Centralized error codes in `ErrorCode` enum:
|
||||
|
||||
```typescript
|
||||
export enum ErrorCode {
|
||||
// Authentication & Authorization
|
||||
UNAUTHORIZED = 'AUTH_001',
|
||||
FORBIDDEN = 'AUTH_002',
|
||||
INVALID_TOKEN = 'AUTH_003',
|
||||
TOKEN_EXPIRED = 'AUTH_004',
|
||||
|
||||
// Validation
|
||||
VALIDATION_ERROR = 'VALIDATION_001',
|
||||
INVALID_FORMAT = 'VALIDATION_002',
|
||||
|
||||
// Resources
|
||||
NOT_FOUND = 'RESOURCE_001',
|
||||
ALREADY_EXISTS = 'RESOURCE_002',
|
||||
CONFLICT = 'RESOURCE_003',
|
||||
|
||||
// Database
|
||||
DATABASE_ERROR = 'DB_001',
|
||||
CONSTRAINT_VIOLATION = 'DB_004',
|
||||
|
||||
// System
|
||||
INTERNAL_ERROR = 'SYS_001',
|
||||
RATE_LIMIT_EXCEEDED = 'SYS_003',
|
||||
}
|
||||
```
|
||||
|
||||
### Using Errors in Services
|
||||
|
||||
```typescript
|
||||
import { NotFoundError, ConflictError } from '../errors/http-error';
|
||||
import { ErrorCode } from '../errors/error-codes';
|
||||
|
||||
export class UserService {
|
||||
async getUserById(id: string) {
|
||||
const user = await this.repository.findById(id);
|
||||
|
||||
if (!user) {
|
||||
throw new NotFoundError('User', { id });
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
async createUser(data: CreateUserInput) {
|
||||
const existing = await this.repository.findByEmail(data.email);
|
||||
|
||||
if (existing) {
|
||||
throw new ConflictError('User with this email already exists');
|
||||
}
|
||||
|
||||
return await this.repository.create(data);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Error Middleware Pattern
|
||||
|
||||
Global error handler middleware processes all errors:
|
||||
|
||||
```typescript
|
||||
export const errorHandler = (
|
||||
err: any,
|
||||
req: express.Request,
|
||||
res: express.Response,
|
||||
_next: express.NextFunction
|
||||
): void => {
|
||||
let statusCode = 500;
|
||||
let errorCode = ErrorCode.INTERNAL_ERROR;
|
||||
let message = 'Internal server error';
|
||||
let isOperational = false;
|
||||
|
||||
// Handle HttpError instances
|
||||
if (err instanceof HttpError) {
|
||||
statusCode = err.statusCode;
|
||||
errorCode = err.errorCode as ErrorCode;
|
||||
message = err.message;
|
||||
isOperational = err.isOperational;
|
||||
}
|
||||
// Handle Prisma errors
|
||||
else if (err.code === 'P2002') {
|
||||
statusCode = 409;
|
||||
errorCode = ErrorCode.CONSTRAINT_VIOLATION;
|
||||
message = 'Resource already exists';
|
||||
isOperational = true;
|
||||
}
|
||||
// Handle Zod validation errors
|
||||
else if (err.name === 'ZodError') {
|
||||
statusCode = 422;
|
||||
errorCode = ErrorCode.VALIDATION_ERROR;
|
||||
message = 'Validation failed';
|
||||
// Extract validation details
|
||||
}
|
||||
|
||||
// Log error
|
||||
if (!isOperational || statusCode >= 500) {
|
||||
logger.error('Unhandled error', { error: err, statusCode, errorCode });
|
||||
} else {
|
||||
logger.warn('Operational error', { error: err, statusCode, errorCode });
|
||||
}
|
||||
|
||||
// Send response
|
||||
const response = {
|
||||
success: false,
|
||||
error: {
|
||||
code: errorCode,
|
||||
message: isProduction && statusCode >= 500
|
||||
? 'Internal server error'
|
||||
: message,
|
||||
},
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
res.status(statusCode).json(response);
|
||||
};
|
||||
```
|
||||
|
||||
### Async Error Wrapper
|
||||
|
||||
Wrap async route handlers to catch promise rejections:
|
||||
|
||||
```typescript
|
||||
export const asyncHandler = (fn: Function) => {
|
||||
return (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
Promise.resolve(fn(req, res, next)).catch(next);
|
||||
};
|
||||
};
|
||||
|
||||
// Usage
|
||||
router.get('/users/:id', asyncHandler(async (req, res) => {
|
||||
const user = await userService.getUserById(req.params.id);
|
||||
res.json({ success: true, data: user });
|
||||
}));
|
||||
```
|
||||
|
||||
### Error Response Format
|
||||
|
||||
Standardized error response format:
|
||||
|
||||
```typescript
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
code: "RESOURCE_001",
|
||||
message: "User not found",
|
||||
details?: {
|
||||
// Optional additional details (not in production for 5xx errors)
|
||||
}
|
||||
},
|
||||
timestamp: "2024-01-01T00:00:00.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use Specific Error Classes**: Use the most specific error class available
|
||||
2. **Include Context**: Provide helpful error messages with context
|
||||
3. **Mark Operational Errors**: Set `isOperational: true` for expected errors
|
||||
4. **Don't Expose Internal Details**: Hide implementation details in production
|
||||
5. **Log Appropriately**: Use `logger.error()` for programming errors, `logger.warn()` for operational errors
|
||||
6. **Handle Database Errors**: Map Prisma errors to appropriate HTTP errors
|
||||
7. **Use Error Codes**: Always use `ErrorCode` enum for consistency
|
||||
8. **Validate Early**: Validate input early to catch errors before processing
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
1. **Not Using Error Classes**: Using generic `Error` instead of specific error classes
|
||||
2. **Exposing Stack Traces**: Including stack traces in production responses
|
||||
3. **Ignoring Errors**: Not handling errors in async operations
|
||||
4. **Generic Error Messages**: Using vague error messages without context
|
||||
5. **Not Logging**: Forgetting to log errors for debugging
|
||||
6. **Wrong HTTP Status Codes**: Using incorrect status codes for error types
|
||||
7. **Not Using Error Middleware**: Handling errors manually instead of using middleware
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Error Not Caught by Middleware
|
||||
|
||||
**Problem**: Error not being caught by error middleware
|
||||
**Solution**: Ensure error middleware is added last, after all routes. Use `asyncHandler` for async route handlers.
|
||||
|
||||
### Generic Error Messages in Production
|
||||
|
||||
**Problem**: Generic "Internal server error" shown even for operational errors
|
||||
**Solution**: Check `isOperational` flag is set correctly. Verify error middleware handles all error types.
|
||||
|
||||
### Error Code Not Found
|
||||
|
||||
**Problem**: Error code not in `ErrorCode` enum
|
||||
**Solution**: Add error code to enum following naming convention. Update `ERROR_CODE_TO_STATUS` mapping.
|
||||
|
||||
### Stack Traces Exposed
|
||||
|
||||
**Problem**: Stack traces visible in API responses
|
||||
**Solution**: Ensure production environment checks are in place. Use error middleware to filter stack traces.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Error Class | Status | When to Use |
|
||||
|-------------|--------|-------------|
|
||||
| `NotFoundError` | 404 | Resource not found |
|
||||
| `BadRequestError` | 400 | Invalid request |
|
||||
| `ValidationError` | 422 | Input validation failed |
|
||||
| `UnauthorizedError` | 401 | Auth required |
|
||||
| `ForbiddenError` | 403 | Access denied |
|
||||
| `ConflictError` | 409 | Duplicate/conflict |
|
||||
| `RateLimitError` | 429 | Too many requests |
|
||||
| `InternalServerError` | 500 | Server error |
|
||||
|
||||
**Error Response Format:**
|
||||
```typescript
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
code: "RESOURCE_001",
|
||||
message: "User not found",
|
||||
details?: { /* validation details */ }
|
||||
},
|
||||
timestamp: "2024-01-01T00:00:00.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
**Common Error Codes:**
|
||||
| Code | Category | Description |
|
||||
|------|----------|-------------|
|
||||
| `AUTH_001` | Auth | Unauthorized |
|
||||
| `AUTH_003` | Auth | Invalid token |
|
||||
| `VALIDATION_001` | Validation | Validation failed |
|
||||
| `RESOURCE_001` | Resource | Not found |
|
||||
| `RESOURCE_002` | Resource | Already exists |
|
||||
| `DB_001` | Database | Database error |
|
||||
| `SYS_001` | System | Internal error |
|
||||
|
||||
**Essential Imports:**
|
||||
```typescript
|
||||
import { NotFoundError, ValidationError, ConflictError } from '../errors/http-error';
|
||||
import { ErrorCode } from '../errors/error-codes';
|
||||
import { asyncHandler } from '../middlewares/async.middleware';
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
- [Error Classes](../../services/iam-service/src/errors/http-error.ts) - Base error classes
|
||||
- [Error Codes](../../services/iam-service/src/errors/error-codes.ts) - Error code definitions
|
||||
- [Error Middleware](../../services/iam-service/src/middlewares/error.middleware.ts) - Global error handler
|
||||
- [API Design](../api-design/SKILL.md) - API response formats
|
||||
- [Security](../security/SKILL.md) - Security error handling
|
||||
@@ -1,270 +0,0 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
# Event-Driven Architecture Patterns
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use this skill when:
|
||||
- Implementing asynchronous communication between services
|
||||
- Decoupling services for better scalability
|
||||
- Publishing domain events for downstream consumers
|
||||
- Consuming events from other services
|
||||
- Implementing event sourcing patterns
|
||||
- Implementing CQRS (Command Query Responsibility Segregation)
|
||||
- Exposing event streams via HTTP (SSE/WebSocket)
|
||||
- Handling eventual consistency across services
|
||||
- Building reactive systems that respond to changes
|
||||
- Integrating with Apache Kafka message broker
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Event-Driven vs Request-Response
|
||||
|
||||
| Aspect | Request-Response | Event-Driven |
|
||||
|--------|------------------|--------------|
|
||||
| Communication | Synchronous | Asynchronous |
|
||||
| Coupling | Tight | Loose |
|
||||
| Blocking | Yes | No |
|
||||
| Consistency | Immediate | Eventual |
|
||||
| Infrastructure | Traefik API Gateway | Kafka |
|
||||
|
||||
### Kafka Fundamentals
|
||||
|
||||
**Topics**: Named streams of events (e.g., `user.created`, `order.placed`)
|
||||
- Organized by domain and action
|
||||
- Divided into partitions for parallelism
|
||||
|
||||
**Partitions**: Physical division of topics
|
||||
- Enables horizontal scaling
|
||||
- Maintains ordering per partition key
|
||||
- Multiple consumers can process different partitions
|
||||
|
||||
**Consumer Groups**: Group of consumers working together
|
||||
- Each partition consumed by only one consumer in group
|
||||
- Enables parallel processing
|
||||
- Automatically rebalances on consumer join/leave
|
||||
|
||||
**Producers**: Services that publish events to topics
|
||||
|
||||
**Consumers**: Services that subscribe to topics and process events
|
||||
|
||||
### Event Structure
|
||||
|
||||
```typescript
|
||||
interface BaseEvent {
|
||||
eventId: string; // Unique event identifier
|
||||
eventType: string; // Event type (e.g., "user.created")
|
||||
eventVersion: string; // Schema version (e.g., "1.0.0")
|
||||
timestamp: string; // ISO 8601 timestamp
|
||||
source: string; // Service that published the event
|
||||
correlationId?: string; // Request correlation ID
|
||||
traceId?: string; // Distributed tracing ID
|
||||
data: unknown; // Event payload
|
||||
}
|
||||
```
|
||||
|
||||
### Event Naming Conventions
|
||||
|
||||
**Event Type Format**: `{domain}.{action}.v{version}`
|
||||
- `user.created.v1`
|
||||
- `order.placed.v1`
|
||||
- `payment.processed.v2`
|
||||
|
||||
**Topic Naming**: `{domain}.{entity}.{action}`
|
||||
- `user.created`
|
||||
- `order.placed`
|
||||
- `payment.processed`
|
||||
|
||||
## Key Patterns
|
||||
|
||||
### 1. Event Publishing
|
||||
|
||||
```typescript
|
||||
// Fire-and-forget with error logging
|
||||
eventPublisher.publish('user.created', event, { partitionKey: user.id })
|
||||
.catch(err => logger.error('Failed to publish', { err }));
|
||||
```
|
||||
|
||||
### 2. Event Consuming
|
||||
|
||||
```typescript
|
||||
consumer.on('user.created', {
|
||||
handle: async (event) => {
|
||||
await processEvent(event);
|
||||
},
|
||||
});
|
||||
await consumer.start(['user.created']);
|
||||
```
|
||||
|
||||
### 3. Outbox Pattern (Transactional)
|
||||
|
||||
Store events in database within same transaction, then publish asynchronously:
|
||||
```typescript
|
||||
await prisma.$transaction(async (tx) => {
|
||||
const user = await tx.user.create({ data });
|
||||
await outboxService.addToOutbox('user.created', userData, 'user.created');
|
||||
return user;
|
||||
});
|
||||
```
|
||||
|
||||
### 4. Dead Letter Queue (DLQ)
|
||||
|
||||
After max retries, send failed events to DLQ topic for manual inspection:
|
||||
```typescript
|
||||
after maxRetries → send to topic.dlq
|
||||
```
|
||||
|
||||
### 5. Idempotency
|
||||
|
||||
Consumers must handle duplicate events:
|
||||
```typescript
|
||||
if (await this.isProcessed(event.eventId)) return;
|
||||
await processEvent(event);
|
||||
await this.markProcessed(event.eventId);
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Partition Key Selection
|
||||
- Use entity ID for ordering guarantees (same entity → same partition)
|
||||
- Use correlation ID for request tracing
|
||||
- Use user ID for user-scoped events
|
||||
- Avoid high-cardinality keys (distributes evenly)
|
||||
|
||||
### Event Ordering Guarantees
|
||||
- Kafka guarantees ordering **per partition**
|
||||
- Use partition key to ensure related events go to same partition
|
||||
- Events in different partitions have no ordering guarantee
|
||||
- Don't rely on global ordering across all events
|
||||
|
||||
### Event Size Limits
|
||||
- Recommended: < 1MB per event
|
||||
- Kafka default: 1MB (configurable)
|
||||
- For large payloads: Store data elsewhere, send reference in event
|
||||
|
||||
### Performance Optimization
|
||||
- **Batch Publishing**: Group multiple events for better throughput
|
||||
- **Async Publishing**: Don't block request handlers
|
||||
- **Consumer Parallelism**: Use multiple partitions and consumers
|
||||
- **Connection Pooling**: Reuse Kafka client instances
|
||||
- **Compression**: Enable compression for better network usage
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
1. **Blocking on Publish**: Slowing down request handlers
|
||||
```typescript
|
||||
// BAD: Await in request handler
|
||||
await eventPublisher.publish('user.created', event);
|
||||
res.json({ success: true });
|
||||
|
||||
// GOOD: Fire and forget with error logging
|
||||
eventPublisher.publish('user.created', event)
|
||||
.catch(err => logger.error('Failed to publish', { err }));
|
||||
res.json({ success: true });
|
||||
```
|
||||
|
||||
2. **No Idempotency**: Duplicate event processing issues
|
||||
```typescript
|
||||
// BAD: No duplicate check
|
||||
async handle(event) {
|
||||
await createUser(event.data);
|
||||
}
|
||||
|
||||
// GOOD: Check for duplicates
|
||||
async handle(event) {
|
||||
if (await this.isProcessed(event.eventId)) return;
|
||||
await createUser(event.data);
|
||||
await this.markProcessed(event.eventId);
|
||||
}
|
||||
```
|
||||
|
||||
3. **Missing Partition Key**: Events for same entity out of order
|
||||
```typescript
|
||||
// BAD: No partition key
|
||||
await publish('user.updated', event);
|
||||
|
||||
// GOOD: Use entity ID as partition key
|
||||
await publish('user.updated', event, { partitionKey: userId });
|
||||
```
|
||||
|
||||
4. **No Dead Letter Queue**: Lost events on failure
|
||||
```typescript
|
||||
// GOOD: Always implement DLQ for failed events
|
||||
after maxRetries → send to topic.dlq
|
||||
```
|
||||
|
||||
5. **Breaking Schema Changes**: Use versioning strategy instead
|
||||
|
||||
6. **Global Ordering Expectations**: Understand partition ordering only
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Concept | Description |
|
||||
|---------|-------------|
|
||||
| **Topic** | Named stream of events (e.g., `user.created`) |
|
||||
| **Partition** | Division of topic for parallelism |
|
||||
| **Consumer Group** | Consumers sharing workload |
|
||||
| **Offset** | Position in partition |
|
||||
|
||||
**Event Structure:**
|
||||
```typescript
|
||||
{
|
||||
eventId: "uuid", // Unique identifier
|
||||
eventType: "user.created", // Event type
|
||||
eventVersion: "1.0.0", // Schema version
|
||||
timestamp: "ISO-8601", // When published
|
||||
source: "auth-service", // Publisher service
|
||||
correlationId: "uuid", // Request trace ID
|
||||
data: { ... } // Event payload
|
||||
}
|
||||
```
|
||||
|
||||
**Topic Naming:**
|
||||
```
|
||||
{domain}.{action}
|
||||
user.created
|
||||
order.placed
|
||||
payment.processed
|
||||
```
|
||||
|
||||
**Essential Commands:**
|
||||
```bash
|
||||
# List topics
|
||||
kafka-topics --list --bootstrap-server localhost:9092
|
||||
|
||||
# Create topic
|
||||
kafka-topics --create --topic user.created --partitions 3
|
||||
|
||||
# Consume from beginning
|
||||
kafka-console-consumer --topic user.created --from-beginning
|
||||
```
|
||||
|
||||
**KafkaJS Quick Setup:**
|
||||
```typescript
|
||||
import { Kafka } from 'kafkajs';
|
||||
|
||||
const kafka = new Kafka({ brokers: ['localhost:9092'], clientId: 'my-app' });
|
||||
const producer = kafka.producer();
|
||||
const consumer = kafka.consumer({ groupId: 'my-group' });
|
||||
```
|
||||
|
||||
**Environment Variables:**
|
||||
```bash
|
||||
KAFKA_BROKERS=localhost:9092
|
||||
KAFKA_CLIENT_ID=my-service
|
||||
KAFKA_CONSUMER_GROUP_ID=my-service-consumers
|
||||
SCHEMA_REGISTRY_URL=http://localhost:8081
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
- [Detailed Reference](./references/REFERENCE.md) - Full code examples and implementation details
|
||||
- [KafkaJS Documentation](https://kafka.js.org/) - Node.js Kafka client
|
||||
- [Confluent Schema Registry](https://docs.confluent.io/platform/current/schema-registry/index.html) - Schema versioning
|
||||
- [Kafka Best Practices](https://kafka.apache.org/documentation/#best_practices) - Official Kafka documentation
|
||||
- [Resilience Patterns](../resilience-patterns/SKILL.md) - Circuit breaker, retry patterns
|
||||
- [Error Handling Patterns](../error-handling-patterns/SKILL.md) - Error handling best practices
|
||||
- [Observability & Monitoring](../observability-monitoring/SKILL.md) - Logging, metrics, tracing
|
||||
- [Project Rules](../project-rules/SKILL.md) - GoodGo coding standards
|
||||
@@ -1,415 +0,0 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
# Infrastructure as Code Patterns
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use this skill when:
|
||||
- Managing infrastructure with code
|
||||
- Implementing Terraform modules
|
||||
- Setting up GitOps workflows
|
||||
- Creating Kubernetes operators
|
||||
- Testing infrastructure changes
|
||||
- Managing multi-environment infrastructure
|
||||
- Versioning infrastructure
|
||||
- Automating infrastructure provisioning
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Infrastructure as Code Benefits
|
||||
|
||||
1. **Version Control**: Track infrastructure changes
|
||||
2. **Reproducibility**: Consistent environments
|
||||
3. **Automation**: Reduce manual errors
|
||||
4. **Testing**: Test infrastructure changes
|
||||
5. **Collaboration**: Team can review changes
|
||||
|
||||
### IaC Tools
|
||||
|
||||
- **Terraform**: Infrastructure provisioning
|
||||
- **Kubernetes Manifests**: K8s resource definitions
|
||||
- **Helm**: K8s package manager
|
||||
- **ArgoCD/Flux**: GitOps tools
|
||||
|
||||
## Terraform Patterns
|
||||
|
||||
### Module Structure
|
||||
|
||||
```
|
||||
infra/terraform/
|
||||
├── modules/
|
||||
│ ├── kubernetes-cluster/
|
||||
│ │ ├── main.tf
|
||||
│ │ ├── variables.tf
|
||||
│ │ └── outputs.tf
|
||||
│ ├── postgresql/
|
||||
│ │ ├── main.tf
|
||||
│ │ ├── variables.tf
|
||||
│ │ └── outputs.tf
|
||||
│ └── redis/
|
||||
│ ├── main.tf
|
||||
│ ├── variables.tf
|
||||
│ └── outputs.tf
|
||||
├── environments/
|
||||
│ ├── staging/
|
||||
│ │ ├── main.tf
|
||||
│ │ └── terraform.tfvars
|
||||
│ └── production/
|
||||
│ ├── main.tf
|
||||
│ └── terraform.tfvars
|
||||
└── shared/
|
||||
└── backend.tf
|
||||
```
|
||||
|
||||
### Terraform Module Example
|
||||
|
||||
```hcl
|
||||
# infra/terraform/modules/postgresql/main.tf
|
||||
# EN: PostgreSQL module
|
||||
# VI: Module PostgreSQL
|
||||
variable "database_name" {
|
||||
description = "Database name"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "environment" {
|
||||
description = "Environment name"
|
||||
type = string
|
||||
}
|
||||
|
||||
resource "google_sql_database_instance" "postgres" {
|
||||
name = "${var.database_name}-${var.environment}"
|
||||
database_version = "POSTGRES_15"
|
||||
region = "us-central1"
|
||||
|
||||
settings {
|
||||
tier = "db-f1-micro"
|
||||
|
||||
backup_configuration {
|
||||
enabled = true
|
||||
start_time = "03:00"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "google_sql_database" "database" {
|
||||
name = var.database_name
|
||||
instance = google_sql_database_instance.postgres.name
|
||||
}
|
||||
|
||||
output "connection_name" {
|
||||
value = google_sql_database_instance.postgres.connection_name
|
||||
}
|
||||
|
||||
output "database_url" {
|
||||
value = "postgresql://user:pass@${google_sql_database_instance.postgres.ip_address}/${var.database_name}"
|
||||
sensitive = true
|
||||
}
|
||||
```
|
||||
|
||||
### Using Modules
|
||||
|
||||
```hcl
|
||||
# infra/terraform/environments/staging/main.tf
|
||||
# EN: Use PostgreSQL module
|
||||
# VI: Sử dụng module PostgreSQL
|
||||
module "postgresql" {
|
||||
source = "../../modules/postgresql"
|
||||
|
||||
database_name = "goodgo_staging"
|
||||
environment = "staging"
|
||||
}
|
||||
|
||||
output "database_url" {
|
||||
value = module.postgresql.database_url
|
||||
sensitive = true
|
||||
}
|
||||
```
|
||||
|
||||
## GitOps Patterns
|
||||
|
||||
### ArgoCD Setup
|
||||
|
||||
```yaml
|
||||
# infra/gitops/argocd/applications/user-service.yaml
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: user-service
|
||||
namespace: argocd
|
||||
spec:
|
||||
project: default
|
||||
source:
|
||||
repoURL: https://github.com/goodgo/platform
|
||||
targetRevision: main
|
||||
path: deployments/production/kubernetes/user-service
|
||||
destination:
|
||||
server: https://kubernetes.default.svc
|
||||
namespace: production
|
||||
syncPolicy:
|
||||
automated:
|
||||
prune: true
|
||||
selfHeal: true
|
||||
syncOptions:
|
||||
- CreateNamespace=true
|
||||
```
|
||||
|
||||
### Flux Setup
|
||||
|
||||
```yaml
|
||||
# infra/gitops/flux/kustomizations/user-service.yaml
|
||||
apiVersion: kustomize.toolkit.fluxcd.io/v1beta2
|
||||
kind: Kustomization
|
||||
metadata:
|
||||
name: user-service
|
||||
namespace: flux-system
|
||||
spec:
|
||||
interval: 5m
|
||||
path: ./deployments/production/kubernetes/user-service
|
||||
prune: true
|
||||
sourceRef:
|
||||
kind: GitRepository
|
||||
name: platform-repo
|
||||
validation: client
|
||||
```
|
||||
|
||||
## Infrastructure Testing
|
||||
|
||||
### Terraform Testing
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# scripts/infra/test-terraform.sh
|
||||
# EN: Test Terraform changes
|
||||
# VI: Test các thay đổi Terraform
|
||||
|
||||
cd infra/terraform/environments/staging
|
||||
|
||||
# EN: Validate Terraform
|
||||
# VI: Validate Terraform
|
||||
terraform init
|
||||
terraform validate
|
||||
|
||||
# EN: Plan changes
|
||||
# VI: Plan changes
|
||||
terraform plan -out=tfplan
|
||||
|
||||
# EN: Review plan
|
||||
# VI: Review plan
|
||||
terraform show tfplan
|
||||
```
|
||||
|
||||
### Infrastructure Validation
|
||||
|
||||
```typescript
|
||||
// scripts/infra/validate-k8s.ts
|
||||
// EN: Validate Kubernetes manifests
|
||||
// VI: Validate Kubernetes manifests
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
function validateKubernetesManifests(path: string): boolean {
|
||||
try {
|
||||
execSync(`kubectl apply --dry-run=client -f ${path}`, {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
console.log('Kubernetes manifests are valid');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Kubernetes validation failed', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Environment Management
|
||||
|
||||
### Environment Configuration
|
||||
|
||||
```hcl
|
||||
# infra/terraform/environments/staging/terraform.tfvars
|
||||
environment = "staging"
|
||||
cluster_name = "goodgo-staging"
|
||||
node_count = 3
|
||||
instance_type = "t3.medium"
|
||||
|
||||
# infra/terraform/environments/production/terraform.tfvars
|
||||
environment = "production"
|
||||
cluster_name = "goodgo-production"
|
||||
node_count = 5
|
||||
instance_type = "t3.large"
|
||||
```
|
||||
|
||||
### Multi-Environment Module
|
||||
|
||||
```hcl
|
||||
# infra/terraform/modules/service/main.tf
|
||||
variable "environment" {
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "replicas" {
|
||||
type = map(number)
|
||||
default = {
|
||||
staging = 2
|
||||
production = 5
|
||||
}
|
||||
}
|
||||
|
||||
resource "kubernetes_deployment" "service" {
|
||||
metadata {
|
||||
name = "${var.service_name}-${var.environment}"
|
||||
}
|
||||
|
||||
spec {
|
||||
replicas = var.replicas[var.environment]
|
||||
# ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Kubernetes Operators
|
||||
|
||||
### Custom Resource Definition
|
||||
|
||||
```yaml
|
||||
# infra/operators/database-operator/crd.yaml
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
name: databases.example.com
|
||||
spec:
|
||||
group: example.com
|
||||
versions:
|
||||
- name: v1
|
||||
served: true
|
||||
storage: true
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
type: object
|
||||
properties:
|
||||
spec:
|
||||
type: object
|
||||
properties:
|
||||
databaseName:
|
||||
type: string
|
||||
environment:
|
||||
type: string
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Version Control**: Keep all infrastructure in version control
|
||||
2. **Modules**: Create reusable Terraform modules
|
||||
3. **Testing**: Test infrastructure changes before applying
|
||||
4. **GitOps**: Use GitOps for Kubernetes deployments
|
||||
5. **Environment Isolation**: Separate environments completely
|
||||
6. **State Management**: Use remote state backends
|
||||
7. **Secrets**: Never commit secrets, use secrets managers
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
1. **Committing Secrets**: Exposed credentials
|
||||
```hcl
|
||||
# ❌ BAD: Hardcoded secrets
|
||||
password = "my-secret-password"
|
||||
|
||||
# ✅ GOOD: Use variables + secrets manager
|
||||
password = var.db_password # From env or secrets manager
|
||||
```
|
||||
|
||||
2. **No Remote State**: State conflicts in team
|
||||
```hcl
|
||||
# ❌ BAD: Local state
|
||||
# (no backend config)
|
||||
|
||||
# ✅ GOOD: Remote state
|
||||
terraform {
|
||||
backend "s3" {
|
||||
bucket = "terraform-state"
|
||||
key = "staging/terraform.tfstate"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. **No State Locking**: Concurrent modifications
|
||||
```hcl
|
||||
# ✅ Enable state locking
|
||||
backend "s3" {
|
||||
dynamodb_table = "terraform-locks"
|
||||
}
|
||||
```
|
||||
|
||||
4. **Applying Without Plan Review**: Unexpected changes
|
||||
```bash
|
||||
# ❌ BAD: Direct apply
|
||||
terraform apply
|
||||
|
||||
# ✅ GOOD: Plan first, review, then apply
|
||||
terraform plan -out=tfplan
|
||||
terraform show tfplan # Review
|
||||
terraform apply tfplan
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Tool | Purpose | State |
|
||||
|------|---------|-------|
|
||||
| **Terraform** | Cloud resources | Stateful |
|
||||
| **Kubernetes** | Container orchestration | Declarative |
|
||||
| **Helm** | K8s package manager | Template |
|
||||
| **ArgoCD/Flux** | GitOps deployment | Git-synced |
|
||||
|
||||
**Terraform Commands:**
|
||||
```bash
|
||||
terraform init # Initialize
|
||||
terraform validate # Validate syntax
|
||||
terraform plan # Preview changes
|
||||
terraform apply # Apply changes
|
||||
terraform destroy # Remove resources
|
||||
terraform state list # List state resources
|
||||
```
|
||||
|
||||
**Module Structure:**
|
||||
```
|
||||
modules/service/
|
||||
├── main.tf # Resources
|
||||
├── variables.tf # Input variables
|
||||
├── outputs.tf # Output values
|
||||
└── README.md # Documentation
|
||||
```
|
||||
|
||||
**Environment Pattern:**
|
||||
```
|
||||
environments/
|
||||
├── staging/
|
||||
│ ├── main.tf
|
||||
│ └── terraform.tfvars
|
||||
└── production/
|
||||
├── main.tf
|
||||
└── terraform.tfvars
|
||||
```
|
||||
|
||||
**GitOps Workflow:**
|
||||
```
|
||||
Git Push → CI Validates → ArgoCD Syncs → K8s Applied
|
||||
│
|
||||
Auto-heal if drift
|
||||
```
|
||||
|
||||
**Best Practices Checklist:**
|
||||
- [ ] Remote state backend configured
|
||||
- [ ] State locking enabled
|
||||
- [ ] No secrets in code
|
||||
- [ ] Modules for reusable components
|
||||
- [ ] Environment-specific tfvars
|
||||
- [ ] PR review for all changes
|
||||
|
||||
## Resources
|
||||
|
||||
- [Terraform Documentation](https://www.terraform.io/docs)
|
||||
- [ArgoCD Documentation](https://argo-cd.readthedocs.io/)
|
||||
- [Flux Documentation](https://fluxcd.io/docs/)
|
||||
- [Deployment Kubernetes](../deployment-kubernetes/SKILL.md) - K8s patterns
|
||||
- [Project Rules](../project-rules/SKILL.md) - GoodGo standards
|
||||
@@ -1,225 +0,0 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
# Inter-Service Communication Patterns
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use this skill when:
|
||||
- Implementing service-to-service communication
|
||||
- Choosing between REST, gRPC, or GraphQL protocols
|
||||
- Setting up gRPC services and clients
|
||||
- Implementing GraphQL services and resolvers
|
||||
- Implementing service-to-service authentication
|
||||
- Building resilient service clients with circuit breakers
|
||||
- Managing connection pooling for service clients
|
||||
- Implementing request/response interceptors
|
||||
- Handling service discovery for internal calls
|
||||
|
||||
## Protocol Comparison
|
||||
|
||||
| Protocol | Use Case | Latency | Complexity | Best For |
|
||||
|----------|----------|---------|------------|----------|
|
||||
| **REST** | External APIs, CRUD | Medium | Low | Browser clients, simple APIs |
|
||||
| **gRPC** | Internal high-perf | Low | High | Service-to-service, streaming |
|
||||
| **GraphQL** | Flexible queries | Medium | Medium | Mobile apps, complex data |
|
||||
|
||||
### Protocol Selection Guidelines
|
||||
|
||||
```
|
||||
External/Public API → REST
|
||||
Internal service-to-service → gRPC (performance) or REST (simplicity)
|
||||
Complex data fetching → GraphQL
|
||||
Real-time streaming → gRPC or WebSocket
|
||||
```
|
||||
|
||||
## HTTP/REST Client Pattern
|
||||
|
||||
```typescript
|
||||
import axios from 'axios';
|
||||
|
||||
const client = axios.create({
|
||||
baseURL: process.env.USER_SERVICE_URL,
|
||||
timeout: 5000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-service-auth': process.env.INTERNAL_API_KEY,
|
||||
},
|
||||
});
|
||||
|
||||
// Add correlation ID for tracing
|
||||
client.interceptors.request.use((config) => {
|
||||
config.headers['x-correlation-id'] = generateCorrelationId();
|
||||
return config;
|
||||
});
|
||||
```
|
||||
|
||||
## gRPC Pattern
|
||||
|
||||
### Proto Definition
|
||||
```protobuf
|
||||
syntax = "proto3";
|
||||
package goodgo.user.v1;
|
||||
|
||||
service UserService {
|
||||
rpc GetUser(GetUserRequest) returns (GetUserResponse);
|
||||
rpc StreamUserUpdates(StreamRequest) returns (stream UserUpdate);
|
||||
}
|
||||
|
||||
message GetUserRequest { string user_id = 1; }
|
||||
message GetUserResponse { User user = 1; }
|
||||
```
|
||||
|
||||
### Client Usage
|
||||
```typescript
|
||||
const client = new GrpcClient({
|
||||
protoPath: './proto/user_service.proto',
|
||||
packageName: 'goodgo.user.v1',
|
||||
serviceName: 'UserService',
|
||||
serverUrl: 'localhost:50051',
|
||||
});
|
||||
|
||||
const user = await client.call('getUser', { user_id: '123' });
|
||||
```
|
||||
|
||||
## GraphQL Pattern
|
||||
|
||||
### Client Usage
|
||||
```typescript
|
||||
const client = new GraphQLClient(process.env.USER_SERVICE_GRAPHQL_URL, {
|
||||
headers: { 'x-service-auth': process.env.INTERNAL_API_KEY },
|
||||
});
|
||||
|
||||
const GET_USER = `query GetUser($id: ID!) { user(id: $id) { id email name } }`;
|
||||
const data = await client.request(GET_USER, { id: '123' });
|
||||
```
|
||||
|
||||
## Service Authentication
|
||||
|
||||
### Internal Auth Middleware
|
||||
```typescript
|
||||
export const internalAuthMiddleware = (req, res, next) => {
|
||||
const token = req.headers['x-service-auth'];
|
||||
|
||||
if (token !== process.env.INTERNAL_API_KEY) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
error: { code: 'INVALID_SERVICE_AUTH', message: 'Invalid authentication' }
|
||||
});
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
```
|
||||
|
||||
### Essential Headers
|
||||
|
||||
| Header | Purpose |
|
||||
|--------|---------|
|
||||
| `x-service-auth` | Internal authentication token |
|
||||
| `x-correlation-id` | Request tracing across services |
|
||||
| `x-request-id` | Unique request identification |
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Performance Optimization
|
||||
- **Connection Pooling**: Reuse HTTP connections
|
||||
- **Keep-Alive**: Enable persistent connections
|
||||
- **Compression**: Use gzip for HTTP responses
|
||||
- **Timeouts**: Always set appropriate timeouts
|
||||
- **Circuit Breaker**: Prevent cascade failures
|
||||
|
||||
### Security
|
||||
- **Service Authentication**: Always authenticate internal calls
|
||||
- **TLS/mTLS**: Use TLS for all communication
|
||||
- **Secrets Management**: Use environment variables
|
||||
- **Rate Limiting**: Implement for service clients
|
||||
|
||||
### Observability
|
||||
- **Logging**: Log all calls with correlation IDs
|
||||
- **Metrics**: Track duration, success rate, errors
|
||||
- **Tracing**: Add distributed tracing
|
||||
- **Health Checks**: Monitor service health
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
1. **No Service Authentication**
|
||||
```typescript
|
||||
// BAD: No auth header
|
||||
await client.get('/api/users');
|
||||
|
||||
// GOOD: Include service auth
|
||||
await client.get('/api/users', {
|
||||
headers: { 'x-service-auth': process.env.INTERNAL_API_KEY }
|
||||
});
|
||||
```
|
||||
|
||||
2. **Missing Timeouts**
|
||||
```typescript
|
||||
// BAD: No timeout
|
||||
await axios.get(url);
|
||||
|
||||
// GOOD: Set timeout
|
||||
await axios.get(url, { timeout: 5000 });
|
||||
```
|
||||
|
||||
3. **No Circuit Breaker**
|
||||
```typescript
|
||||
// GOOD: Use circuit breaker for external calls
|
||||
await circuitBreaker.fire(() => serviceClient.get('/api/users'));
|
||||
```
|
||||
|
||||
4. **Hardcoded Service URLs**
|
||||
```typescript
|
||||
// BAD
|
||||
const url = 'http://user-service:5000';
|
||||
|
||||
// GOOD
|
||||
const url = process.env.USER_SERVICE_URL;
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
**HTTP Client Setup:**
|
||||
```typescript
|
||||
const client = axios.create({
|
||||
baseURL: process.env.SERVICE_URL,
|
||||
timeout: 5000,
|
||||
headers: { 'x-service-auth': process.env.INTERNAL_API_KEY }
|
||||
});
|
||||
```
|
||||
|
||||
**gRPC Client Setup:**
|
||||
```typescript
|
||||
const client = new GrpcClient({
|
||||
protoPath: './proto/service.proto',
|
||||
serviceName: 'UserService',
|
||||
serverUrl: 'localhost:50051'
|
||||
});
|
||||
```
|
||||
|
||||
**GraphQL Client Setup:**
|
||||
```typescript
|
||||
const client = new GraphQLClient(endpoint, {
|
||||
headers: { 'x-service-auth': process.env.INTERNAL_API_KEY }
|
||||
});
|
||||
```
|
||||
|
||||
**Error Classes:**
|
||||
```typescript
|
||||
ServiceUnavailableError // Service is down
|
||||
ServiceTimeoutError // Request timeout
|
||||
ServiceError // Generic service error
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
- [Detailed Reference](./references/REFERENCE.md) - Full code examples and implementation details
|
||||
- [gRPC Documentation](https://grpc.io/docs/) - Official gRPC documentation
|
||||
- [GraphQL Documentation](https://graphql.org/learn/) - GraphQL learning resources
|
||||
- [Protocol Buffers](https://developers.google.com/protocol-buffers) - Protocol Buffer guide
|
||||
- [Resilience Patterns](../resilience-patterns/SKILL.md) - Circuit breaker, retry patterns
|
||||
- [Security](../security/SKILL.md) - Authentication, authorization patterns
|
||||
- [API Design](../api-design/SKILL.md) - RESTful API patterns
|
||||
- [Project Rules](../project-rules/SKILL.md) - GoodGo coding standards
|
||||
@@ -1,356 +0,0 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
|
||||
# Microservices Development Process
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use this skill when:
|
||||
- Creating a new microservice from scratch
|
||||
- Migrating or refactoring an existing service
|
||||
- Planning service implementation with multiple phases
|
||||
- Ensuring comprehensive coverage of all development aspects
|
||||
- Need structured approach to service development
|
||||
|
||||
## Development Process Overview
|
||||
|
||||
The microservices development process follows these phases:
|
||||
1. **Planning & Impact Analysis** - Define scope, impact, dependencies
|
||||
2. **Foundation Setup** - Service structure, configs, infrastructure
|
||||
3. **Core Implementation** - Business logic, APIs, data layer
|
||||
4. **Integration** - Routes, middleware, external services
|
||||
5. **Testing** - Unit, integration, E2E tests
|
||||
6. **Documentation** - API docs, README, guides
|
||||
7. **Cleanup & Verification** - Remove temporary files, verify completeness
|
||||
8. **Deployment** - Staging deployment, production deployment
|
||||
|
||||
### Process Flow Diagram
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
Start([Start: New Service Requirements]) --> Phase1[Phase 1: Planning & Impact Analysis]
|
||||
Phase1 --> ImpactCheck{Impact Analysis<br/>Complete?}
|
||||
ImpactCheck -->|No| Phase1
|
||||
ImpactCheck -->|Yes| Phase2[Phase 2: Foundation Setup]
|
||||
|
||||
Phase2 --> FoundationCheck{Service Starts<br/>& Health Check Passes?}
|
||||
FoundationCheck -->|No| Phase2
|
||||
FoundationCheck -->|Yes| Phase3[Phase 3: Core Implementation]
|
||||
|
||||
Phase3 --> ImplementationCheck{Business Logic<br/>Implemented?}
|
||||
ImplementationCheck -->|No| Phase3
|
||||
ImplementationCheck -->|Yes| Phase4[Phase 4: Integration]
|
||||
|
||||
Phase4 --> IntegrationCheck{Routes & Middleware<br/>Working?}
|
||||
IntegrationCheck -->|No| Phase4
|
||||
IntegrationCheck -->|Yes| Phase5[Phase 5: Testing]
|
||||
|
||||
Phase5 --> TestCheck{Tests Pass<br/>& Coverage Met?}
|
||||
TestCheck -->|No| Phase5
|
||||
TestCheck -->|Yes| Phase6[Phase 6: Documentation]
|
||||
|
||||
Phase6 --> DocCheck{Docs<br/>Complete?}
|
||||
DocCheck -->|No| Phase6
|
||||
DocCheck -->|Yes| Phase7[Phase 7: Cleanup & Verification]
|
||||
|
||||
Phase7 --> VerificationCheck{All Checks<br/>Pass?}
|
||||
VerificationCheck -->|No| Phase7
|
||||
VerificationCheck -->|Yes| Phase8[Phase 8: Deployment]
|
||||
|
||||
Phase8 --> DeployCheck{Staging<br/>Deployed?}
|
||||
DeployCheck -->|No| Phase8
|
||||
DeployCheck -->|Yes| Production{Deploy to<br/>Production?}
|
||||
Production -->|Yes| ProdDeploy[Production Deployment]
|
||||
Production -->|No| Complete([Complete])
|
||||
ProdDeploy --> Complete
|
||||
|
||||
style Phase1 fill:#e1f5ff
|
||||
style Phase2 fill:#fff4e1
|
||||
style Phase3 fill:#f0e1ff
|
||||
style Phase4 fill:#e1ffe1
|
||||
style Phase5 fill:#ffe1e1
|
||||
style Phase6 fill:#e1ffff
|
||||
style Phase7 fill:#fff0e1
|
||||
style Phase8 fill:#ffe1f5
|
||||
style Complete fill:#d4edda
|
||||
```
|
||||
|
||||
### Detailed Phase Flow
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
subgraph Planning["Phase 1: Planning"]
|
||||
P1A[Define Scope] --> P1B[Impact Analysis]
|
||||
P1B --> P1C[Dependencies Map]
|
||||
P1C --> P1D[Acceptance Criteria]
|
||||
end
|
||||
|
||||
subgraph Foundation["Phase 2: Foundation"]
|
||||
F2A[Copy Template] --> F2B[Configure Package]
|
||||
F2B --> F2C[Setup Database]
|
||||
F2C --> F2D[Configure Docker]
|
||||
F2D --> F2E[Setup Traefik]
|
||||
end
|
||||
|
||||
subgraph Implementation["Phase 3: Implementation"]
|
||||
I3A[DTOs] --> I3B[Repository]
|
||||
I3B --> I3C[Service]
|
||||
I3C --> I3D[Controller]
|
||||
I3D --> I3E[Module]
|
||||
end
|
||||
|
||||
subgraph Integration["Phase 4: Integration"]
|
||||
IN4A[Register Routes] --> IN4B[Setup Middleware]
|
||||
IN4B --> IN4C[External Services]
|
||||
IN4C --> IN4D[Health Checks]
|
||||
end
|
||||
|
||||
subgraph Testing["Phase 5: Testing"]
|
||||
T5A[Unit Tests] --> T5B[Integration Tests]
|
||||
T5B --> T5C[E2E Tests]
|
||||
T5C --> T5D[Coverage Check]
|
||||
end
|
||||
|
||||
subgraph Documentation["Phase 6: Documentation"]
|
||||
D6A[README] --> D6B[API Docs]
|
||||
D6B --> D6C[Architecture Docs]
|
||||
end
|
||||
|
||||
subgraph Cleanup["Phase 7: Cleanup"]
|
||||
C7A[Remove Temp Files] --> C7B[Update References]
|
||||
C7B --> C7C[Verify Everything]
|
||||
end
|
||||
|
||||
subgraph Deployment["Phase 8: Deployment"]
|
||||
DEP8A[Staging] --> DEP8B[Verification]
|
||||
DEP8B --> DEP8C[Production]
|
||||
end
|
||||
|
||||
Planning --> Foundation
|
||||
Foundation --> Implementation
|
||||
Implementation --> Integration
|
||||
Integration --> Testing
|
||||
Testing --> Documentation
|
||||
Documentation --> Cleanup
|
||||
Cleanup --> Deployment
|
||||
|
||||
style Planning fill:#e1f5ff
|
||||
style Foundation fill:#fff4e1
|
||||
style Implementation fill:#f0e1ff
|
||||
style Integration fill:#e1ffe1
|
||||
style Testing fill:#ffe1e1
|
||||
style Documentation fill:#e1ffff
|
||||
style Cleanup fill:#fff0e1
|
||||
style Deployment fill:#ffe1f5
|
||||
```
|
||||
|
||||
## Phase 1: Planning & Impact Analysis
|
||||
|
||||
### Scope Definition
|
||||
|
||||
Define clearly:
|
||||
- **Service Purpose**: What business capability does it provide?
|
||||
- **API Surface**: What endpoints are needed?
|
||||
- **Data Models**: What data structures are required?
|
||||
- **Dependencies**: What services/packages does it depend on?
|
||||
- **Breaking Changes**: Any backward compatibility concerns?
|
||||
|
||||
### Impact Analysis Checklist
|
||||
|
||||
Before starting implementation, identify all affected areas:
|
||||
|
||||
**Files to Create:**
|
||||
- [ ] Service directory: `services/service-name/`
|
||||
- [ ] Prisma schema: `services/service-name/prisma/schema.prisma`
|
||||
- [ ] Dockerfile: `services/service-name/Dockerfile`
|
||||
- [ ] Service README: `services/service-name/README.md`
|
||||
|
||||
**Files to Update:**
|
||||
- [ ] Root `package.json` workspace config
|
||||
- [ ] `deployments/local/docker-compose.yml` - Add service
|
||||
- [ ] `infra/traefik/dynamic/routes.yml` - Add routes
|
||||
- [ ] `.github/workflows/ci-*.yml` - Add CI workflow (if needed)
|
||||
- [ ] Documentation: `docs/en/guides/`, `docs/vi/guides/`
|
||||
- [ ] Scripts: `scripts/db/*.sh`, `scripts/dev/*.sh` (if service-specific)
|
||||
|
||||
**Infrastructure Changes:**
|
||||
- [ ] Database: New schema/tables
|
||||
- [ ] Redis: New cache keys/patterns (if needed)
|
||||
- [ ] Traefik: New routes and services
|
||||
- [ ] Observability: New service metrics/traces
|
||||
|
||||
**Dependencies:**
|
||||
- [ ] External: Database, Redis, third-party APIs
|
||||
- [ ] Internal: Shared packages (@goodgo/logger, @goodgo/types, etc.)
|
||||
- [ ] Other Services: List dependent services
|
||||
|
||||
## Phase 2: Foundation Setup
|
||||
|
||||
### Service Structure Creation
|
||||
|
||||
**Template Usage:**
|
||||
```bash
|
||||
cp -r services/_template services/new-service-name
|
||||
cd services/new-service-name
|
||||
# Update package.json name to @goodgo/new-service-name
|
||||
```
|
||||
|
||||
**Required Files:**
|
||||
- Service structure from template
|
||||
- `package.json` with correct name and dependencies
|
||||
- `src/config/app.config.ts` - Configuration with Zod validation
|
||||
- `.env.example` - Environment variables template
|
||||
- `prisma/schema.prisma` - Database schema
|
||||
- `Dockerfile` - Container configuration
|
||||
- `jest.config.ts` - Test configuration
|
||||
|
||||
### Database Setup
|
||||
|
||||
```bash
|
||||
# Create initial migration
|
||||
cd services/service-name
|
||||
pnpm prisma migrate dev --name init
|
||||
pnpm prisma generate
|
||||
```
|
||||
|
||||
### Docker & Infrastructure
|
||||
|
||||
**Docker Compose Integration:**
|
||||
Add service to `deployments/local/docker-compose.yml` with:
|
||||
- Build context and dockerfile
|
||||
- Environment variables
|
||||
- Traefik labels for routing
|
||||
- Health check configuration
|
||||
|
||||
**Traefik Routes:**
|
||||
Update `infra/traefik/dynamic/routes.yml` with:
|
||||
- Router rules (PathPrefix)
|
||||
- Service configuration
|
||||
- Middleware chain (CORS, rate-limit, auth)
|
||||
|
||||
### Acceptance Criteria for Phase 2
|
||||
|
||||
- [ ] Service directory created from template
|
||||
- [ ] `package.json` configured correctly
|
||||
- [ ] Environment variables defined
|
||||
- [ ] Prisma schema created and migration run
|
||||
- [ ] Service starts: `pnpm dev` (health check passes)
|
||||
- [ ] Docker build succeeds
|
||||
- [ ] Service accessible via Traefik
|
||||
- [ ] No TypeScript errors: `pnpm typecheck`
|
||||
|
||||
## Phase 3: Core Implementation
|
||||
|
||||
### Module Structure
|
||||
|
||||
Each feature module follows this pattern:
|
||||
```
|
||||
modules/feature-name/
|
||||
├── feature.controller.ts # HTTP handlers
|
||||
├── feature.service.ts # Business logic
|
||||
├── feature.repository.ts # Data access
|
||||
├── feature.dto.ts # Validation schemas (Zod)
|
||||
├── feature.module.ts # Module registration
|
||||
└── index.ts # Public exports
|
||||
```
|
||||
|
||||
### Implementation Flow
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
Start[Start Implementation] --> DTOs[1. Create DTOs<br/>Zod Validation Schemas]
|
||||
DTOs --> Repo[2. Create Repository<br/>Prisma Data Access]
|
||||
Repo --> Service[3. Create Service<br/>Business Logic]
|
||||
Service --> Controller[4. Create Controller<br/>HTTP Handlers]
|
||||
Controller --> Module[5. Create Module<br/>Wire Up Components]
|
||||
Module --> Test[Manual Testing]
|
||||
Test --> Pass{Tests Pass?}
|
||||
Pass -->|No| Repo
|
||||
Pass -->|Yes| Next[Next Feature Module]
|
||||
|
||||
style DTOs fill:#e1f5ff
|
||||
style Repo fill:#fff4e1
|
||||
style Service fill:#f0e1ff
|
||||
style Controller fill:#e1ffe1
|
||||
style Module fill:#ffe1e1
|
||||
```
|
||||
|
||||
### Implementation Order
|
||||
|
||||
1. **DTOs** - Zod schemas for request/response validation
|
||||
2. **Repository** - Prisma-based data access, CRUD operations
|
||||
3. **Service** - Business logic, error handling, validation
|
||||
4. **Controller** - HTTP request handling, standardized responses
|
||||
5. **Module** - Wire up components, export router
|
||||
|
||||
### Code Patterns
|
||||
|
||||
**Repository:** Extend base Repository, use Prisma client for data access
|
||||
**Service:** Inject repository, implement business logic, use logger
|
||||
**Controller:** Handle HTTP requests, validate with DTOs, call services
|
||||
**Module:** Wire up dependencies, export router
|
||||
|
||||
### Acceptance Criteria for Phase 3
|
||||
|
||||
- [ ] All DTOs defined with Zod validation
|
||||
- [ ] Repository methods implemented
|
||||
- [ ] Service business logic implemented
|
||||
- [ ] Controllers handle requests correctly
|
||||
- [ ] Modules configured properly
|
||||
- [ ] No TypeScript errors
|
||||
- [ ] Manual API testing successful
|
||||
|
||||
## Phase 4: Integration
|
||||
|
||||
### Route Registration
|
||||
|
||||
Update `src/routes/index.ts`:
|
||||
- Import feature modules
|
||||
- Create router instances
|
||||
- Register routes with path prefixes
|
||||
- Mount to main app with `/api/v1/service-name` prefix
|
||||
|
||||
### Middleware Setup
|
||||
|
||||
**Required Middlewares (in order):**
|
||||
1. Correlation middleware
|
||||
2. Logging middleware
|
||||
3. Metrics middleware
|
||||
4. CORS middleware
|
||||
5. Rate limiting middleware
|
||||
6. Authentication middleware (if needed)
|
||||
7. Error middleware (always last)
|
||||
|
||||
### External Service Integration
|
||||
|
||||
- HTTP clients: Use `@goodgo/http-client` for external APIs
|
||||
- Redis caching: Implement cache patterns for frequently accessed data
|
||||
- Error handling: Handle external service failures gracefully
|
||||
|
||||
### Acceptance Criteria for Phase 4
|
||||
|
||||
- [ ] All routes registered and accessible
|
||||
- [ ] Middlewares applied in correct order
|
||||
- [ ] Error handling works for all scenarios
|
||||
- [ ] External services integrated (if any)
|
||||
- [ ] Caching implemented (if needed)
|
||||
- [ ] Health check endpoint works: `/health`
|
||||
|
||||
## Phase 5: Testing
|
||||
|
||||
### Test Structure
|
||||
|
||||
**Unit Tests:** Next to source files (`*.test.ts`), mock all dependencies
|
||||
**Integration Tests:** `src/__tests__/`, test component interactions
|
||||
**E2E Tests:** `src/__tests__/*.e2e.ts`, test full API workflows
|
||||
|
||||
### Test Coverage Targets
|
||||
|
||||
- Minimum: 70% coverage (branches, functions, lines, statements)
|
||||
- Critical paths: 90%+ coverage
|
||||
- Repositories: 80%+ coverage
|
||||
- Services: 80%+ coverage
|
||||
- Controllers: 70%+ coverage
|
||||
|
||||
@@ -1,314 +0,0 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
# Middleware Patterns
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use this skill when:
|
||||
- Creating custom Express middleware
|
||||
- Organizing middleware chains and ordering
|
||||
- Implementing authentication/authorization middleware
|
||||
- Creating request/response transformation middleware
|
||||
- Handling cross-cutting concerns (logging, metrics, validation)
|
||||
- Implementing async middleware patterns
|
||||
- Testing middleware implementations
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Middleware Function Signature
|
||||
|
||||
Express middleware functions have this signature:
|
||||
|
||||
```typescript
|
||||
(req: Request, res: Response, next: NextFunction) => void | Promise<void>
|
||||
```
|
||||
|
||||
### Middleware Types
|
||||
|
||||
1. **Application-level**: Applied to all routes (`app.use()`)
|
||||
2. **Router-level**: Applied to specific routes (`router.use()`)
|
||||
3. **Route-level**: Applied to specific route handlers
|
||||
|
||||
### Middleware Execution Order
|
||||
|
||||
**Critical**: Middleware order matters! Execution flows top-to-bottom:
|
||||
|
||||
```
|
||||
Request → Middleware 1 → Middleware 2 → ... → Route Handler → Response
|
||||
```
|
||||
|
||||
## Patterns
|
||||
|
||||
### Middleware Chain Order
|
||||
|
||||
Standard middleware order in GoodGo services:
|
||||
|
||||
```typescript
|
||||
// 1. Security (Helmet, CORS)
|
||||
app.use(helmet());
|
||||
app.use(cors({ ... }));
|
||||
|
||||
// 2. Rate Limiting
|
||||
app.use('/api', rateLimitMiddleware);
|
||||
|
||||
// 3. Correlation ID (early for tracing)
|
||||
app.use(correlationMiddleware());
|
||||
|
||||
// 4. Body Parsing
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
app.use(cookieParser());
|
||||
|
||||
// 5. Request Logging
|
||||
app.use(loggerMiddleware);
|
||||
|
||||
// 6. Metrics
|
||||
app.use(metricsMiddleware);
|
||||
|
||||
// 7. Routes
|
||||
app.use(createRouter());
|
||||
|
||||
// 8. Error Handling (ALWAYS LAST)
|
||||
app.use(notFoundHandler);
|
||||
app.use(errorHandler);
|
||||
```
|
||||
|
||||
### Correlation Middleware Pattern
|
||||
|
||||
Adds correlation ID for request tracing:
|
||||
|
||||
```typescript
|
||||
export const correlationMiddleware = () => {
|
||||
return (req: Request, res: Response, next: NextFunction) => {
|
||||
const correlationId = req.headers['x-correlation-id'] || generateId();
|
||||
req.correlationId = correlationId;
|
||||
res.setHeader('x-correlation-id', correlationId);
|
||||
next();
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
### Authentication Middleware Pattern
|
||||
|
||||
Verifies JWT tokens and attaches user to request:
|
||||
|
||||
```typescript
|
||||
export const authenticate = () => {
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const token = extractToken(req);
|
||||
if (!token) {
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
|
||||
const payload = await jwtService.verify(token);
|
||||
req.user = payload;
|
||||
next();
|
||||
} catch (error) {
|
||||
return res.status(401).json({ error: 'Invalid token' });
|
||||
}
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
### Validation Middleware Pattern
|
||||
|
||||
Validates request data using Zod:
|
||||
|
||||
```typescript
|
||||
export const validateDto = (schema: AnyZodObject, property: 'body' | 'query' | 'params' = 'body') => {
|
||||
return (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const validatedData = schema.parse(req[property]);
|
||||
(req as any)[property] = validatedData;
|
||||
next();
|
||||
} catch (error) {
|
||||
if (error instanceof ZodError) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: {
|
||||
code: 'VALIDATION_ERROR',
|
||||
details: error.errors,
|
||||
},
|
||||
});
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
### Conditional Middleware
|
||||
|
||||
Apply middleware conditionally:
|
||||
|
||||
```typescript
|
||||
const conditionalAuth = (options: { optional?: boolean } = {}) => {
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const token = extractToken(req);
|
||||
if (token) {
|
||||
const payload = await jwtService.verify(token);
|
||||
req.user = payload;
|
||||
} else if (!options.optional) {
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
next();
|
||||
} catch (error) {
|
||||
if (!options.optional) {
|
||||
return res.status(401).json({ error: 'Invalid token' });
|
||||
}
|
||||
next();
|
||||
}
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
### Async Middleware Pattern
|
||||
|
||||
Handle async operations properly:
|
||||
|
||||
```typescript
|
||||
export const asyncMiddleware = (fn: Function) => {
|
||||
return (req: Request, res: Response, next: NextFunction) => {
|
||||
Promise.resolve(fn(req, res, next)).catch(next);
|
||||
};
|
||||
};
|
||||
|
||||
// Usage
|
||||
app.get('/users', asyncMiddleware(async (req, res) => {
|
||||
const users = await userService.findAll();
|
||||
res.json({ success: true, data: users });
|
||||
}));
|
||||
```
|
||||
|
||||
### Request/Response Transformation
|
||||
|
||||
Transform request or response data:
|
||||
|
||||
```typescript
|
||||
export const transformResponse = () => {
|
||||
return (req: Request, res: Response, next: NextFunction) => {
|
||||
const originalJson = res.json.bind(res);
|
||||
|
||||
res.json = function(data: unknown) {
|
||||
const transformed = {
|
||||
success: true,
|
||||
data,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
return originalJson(transformed);
|
||||
};
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
### Logging Middleware Pattern
|
||||
|
||||
Log request details:
|
||||
|
||||
```typescript
|
||||
export const requestLogger = (req: Request, res: Response, next: NextFunction) => {
|
||||
const startTime = Date.now();
|
||||
|
||||
res.on('finish', () => {
|
||||
const duration = Date.now() - startTime;
|
||||
logger.info('Request completed', {
|
||||
method: req.method,
|
||||
url: req.url,
|
||||
statusCode: res.statusCode,
|
||||
duration,
|
||||
correlationId: req.correlationId,
|
||||
});
|
||||
});
|
||||
|
||||
next();
|
||||
};
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Order Matters**: Place middleware in correct order (security → correlation → parsing → logging → routes → errors)
|
||||
2. **Error Handling**: Always handle errors and call `next(error)` for error middleware
|
||||
3. **Async Support**: Wrap async middleware properly to catch promise rejections
|
||||
4. **Early Returns**: Use early returns for validation failures (don't call `next()`)
|
||||
5. **Request Extension**: Use TypeScript declaration merging to extend Request type
|
||||
6. **Conditional Logic**: Use middleware factories for conditional middleware
|
||||
7. **Reusability**: Create reusable middleware functions
|
||||
8. **Performance**: Keep middleware lightweight, avoid heavy operations
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
1. **Wrong Order**: Placing middleware in incorrect order (e.g., error handler before routes)
|
||||
2. **Not Calling Next**: Forgetting to call `next()` or `next(error)`
|
||||
3. **Async Errors**: Not handling promise rejections in async middleware
|
||||
4. **Early Return Issues**: Calling `next()` after sending response
|
||||
5. **Type Safety**: Not extending Express Request type properly
|
||||
6. **Performance**: Doing heavy operations in middleware
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Middleware Not Executing
|
||||
|
||||
**Problem**: Middleware not being called
|
||||
**Solution**: Check middleware order, ensure it's added before routes. Verify `next()` is called.
|
||||
|
||||
### Async Errors Not Caught
|
||||
|
||||
**Problem**: Unhandled promise rejections in async middleware
|
||||
**Solution**: Use `asyncHandler` wrapper or wrap async code in try-catch with `next(error)`.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
**Middleware Order (Critical):**
|
||||
```
|
||||
1. Security (helmet, cors)
|
||||
2. Rate Limiting
|
||||
3. Correlation ID
|
||||
4. Body Parsing (json, urlencoded)
|
||||
5. Request Logging
|
||||
6. Metrics
|
||||
7. Routes
|
||||
8. Error Handling (LAST)
|
||||
```
|
||||
|
||||
| Middleware Type | Signature | When to Use |
|
||||
|-----------------|-----------|-------------|
|
||||
| **Standard** | `(req, res, next) => void` | Sync operations |
|
||||
| **Async** | `async (req, res, next) => Promise<void>` | Async operations |
|
||||
| **Error** | `(err, req, res, next) => void` | Error handling |
|
||||
|
||||
**Common Patterns:**
|
||||
```typescript
|
||||
// Async wrapper
|
||||
const asyncHandler = (fn) => (req, res, next) =>
|
||||
Promise.resolve(fn(req, res, next)).catch(next);
|
||||
|
||||
// Conditional middleware
|
||||
const conditionalMiddleware = (condition, middleware) =>
|
||||
condition ? middleware : (req, res, next) => next();
|
||||
|
||||
// Factory pattern
|
||||
const authenticate = (options = {}) => async (req, res, next) => { /* ... */ };
|
||||
```
|
||||
|
||||
**Essential Imports:**
|
||||
```typescript
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { z, ZodError } from 'zod';
|
||||
import { logger } from '@goodgo/logger';
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
- [Express Middleware](https://expressjs.com/en/guide/writing-middleware.html) - Express middleware guide
|
||||
- [Error Handling](../error-handling-patterns/SKILL.md) - Error middleware patterns
|
||||
- [Security](../security/SKILL.md) - Auth middleware patterns
|
||||
- [API Design](../api-design/SKILL.md) - Request/response patterns
|
||||
- [Resilience Patterns](../resilience-patterns/SKILL.md) - Circuit breaker middleware
|
||||
- [Observability & Monitoring](../observability-monitoring/SKILL.md) - Logging middleware
|
||||
- [Project Rules](../project-rules/SKILL.md) - GoodGo standards
|
||||
@@ -1,195 +0,0 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
# Observability & Monitoring Patterns
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use this skill when:
|
||||
- Setting up logging infrastructure
|
||||
- Implementing metrics collection
|
||||
- Adding distributed tracing
|
||||
- Creating health check endpoints
|
||||
- Setting up monitoring dashboards
|
||||
- Debugging production issues
|
||||
- Implementing alerting rules
|
||||
- Analyzing performance bottlenecks
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Three Pillars of Observability
|
||||
1. **Logs**: Event records for debugging
|
||||
2. **Metrics**: Numerical measurements over time
|
||||
3. **Traces**: Request flow across services
|
||||
|
||||
### Tech Stack
|
||||
- **Logging**: Winston, Pino
|
||||
- **Metrics**: Prometheus + Grafana
|
||||
- **Tracing**: OpenTelemetry + Jaeger
|
||||
- **APM**: DataDog or New Relic (optional)
|
||||
|
||||
## Key Patterns
|
||||
|
||||
### Structured Logging
|
||||
|
||||
```typescript
|
||||
import winston from 'winston';
|
||||
|
||||
export const logger = winston.createLogger({
|
||||
level: process.env.LOG_LEVEL || 'info',
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp(),
|
||||
winston.format.json()
|
||||
),
|
||||
defaultMeta: { service: process.env.SERVICE_NAME }
|
||||
});
|
||||
|
||||
// Request logging middleware
|
||||
export const requestLogger = (req, res, next) => {
|
||||
const start = Date.now();
|
||||
res.on('finish', () => {
|
||||
logger.info('HTTP Request', {
|
||||
method: req.method, url: req.url,
|
||||
status: res.statusCode, duration: Date.now() - start,
|
||||
correlationId: req.headers['x-correlation-id']
|
||||
});
|
||||
});
|
||||
next();
|
||||
};
|
||||
```
|
||||
|
||||
### Metrics Collection
|
||||
|
||||
```typescript
|
||||
import { Counter, Histogram, Gauge } from 'prom-client';
|
||||
|
||||
export const httpRequestDuration = new Histogram({
|
||||
name: 'http_request_duration_seconds',
|
||||
help: 'Duration of HTTP requests in seconds',
|
||||
labelNames: ['method', 'route', 'status'],
|
||||
buckets: [0.1, 0.3, 0.5, 0.7, 1, 3, 5, 7, 10]
|
||||
});
|
||||
|
||||
export const httpRequestTotal = new Counter({
|
||||
name: 'http_requests_total',
|
||||
help: 'Total number of HTTP requests',
|
||||
labelNames: ['method', 'route', 'status']
|
||||
});
|
||||
```
|
||||
|
||||
### Distributed Tracing
|
||||
|
||||
```typescript
|
||||
import { NodeSDK } from '@opentelemetry/sdk-node';
|
||||
import { trace, SpanStatusCode } from '@opentelemetry/api';
|
||||
|
||||
export const tracedOperation = async (name: string, fn: Function) => {
|
||||
const span = trace.getTracer('app').startSpan(name);
|
||||
try {
|
||||
const result = await fn();
|
||||
span.setStatus({ code: SpanStatusCode.OK });
|
||||
return result;
|
||||
} catch (error) {
|
||||
span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
|
||||
throw error;
|
||||
} finally {
|
||||
span.end();
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### Health Checks
|
||||
|
||||
```typescript
|
||||
// Liveness - is the service running?
|
||||
app.get('/health/live', (req, res) => res.json({ status: 'ok' }));
|
||||
|
||||
// Readiness - is the service ready for traffic?
|
||||
app.get('/health/ready', async (req, res) => {
|
||||
const dbOk = await prisma.$queryRaw`SELECT 1`.then(() => true).catch(() => false);
|
||||
const redisOk = await redis.ping().then(() => true).catch(() => false);
|
||||
const ready = dbOk && redisOk;
|
||||
res.status(ready ? 200 : 503).json({ ready, db: dbOk, redis: redisOk });
|
||||
});
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
- **Logging**: Use structured JSON format with correlation IDs
|
||||
- **Metrics**: Use standard types (Counter, Gauge, Histogram) with low-cardinality labels
|
||||
- **Tracing**: Add traces for critical operations, sample appropriately
|
||||
- **Alerting**: Alert on symptoms, include runbook links, avoid alert fatigue
|
||||
- **Security**: Never log sensitive data (passwords, tokens, PII)
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
1. **Logging Sensitive Data**: Exposing PII in logs
|
||||
```typescript
|
||||
// BAD: logger.info('User login', { email, password, token });
|
||||
// GOOD: logger.info('User login', { email, userId });
|
||||
```
|
||||
|
||||
2. **High Cardinality Labels**: Too many metric label values
|
||||
```typescript
|
||||
// BAD: httpRequests.labels(method, route, userId).inc();
|
||||
// GOOD: httpRequests.labels(method, route, statusCode).inc();
|
||||
```
|
||||
|
||||
3. **No Correlation IDs**: Can't trace requests across services
|
||||
```typescript
|
||||
// GOOD: Include correlationId in all logs
|
||||
logger.info('Processing', { correlationId: req.headers['x-correlation-id'] });
|
||||
```
|
||||
|
||||
4. **Wrong Log Levels**: Using ERROR for non-errors
|
||||
```typescript
|
||||
// BAD: logger.error('User not found');
|
||||
// GOOD: logger.info('User not found', { userId });
|
||||
```
|
||||
|
||||
5. **No Health Checks**: Service status unknown
|
||||
```typescript
|
||||
// GOOD: Implement both endpoints
|
||||
app.get('/health/live', livenessCheck);
|
||||
app.get('/health/ready', readinessCheck);
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Pillar | Tool | Endpoint |
|
||||
|--------|------|----------|
|
||||
| **Logs** | Winston/Loki | stdout -> Loki |
|
||||
| **Metrics** | Prometheus | `/metrics` |
|
||||
| **Traces** | Jaeger | `http://jaeger:14268` |
|
||||
|
||||
**Log Levels:**
|
||||
| Level | When to Use |
|
||||
|-------|-------------|
|
||||
| `error` | Errors requiring attention |
|
||||
| `warn` | Potential issues, degradation |
|
||||
| `info` | Business events, state changes |
|
||||
| `debug` | Development details |
|
||||
|
||||
**Essential Metrics:**
|
||||
```typescript
|
||||
rate(http_requests_total[5m]) // Request rate
|
||||
rate(http_requests_total{status=~"5.."}[5m]) // Error rate
|
||||
histogram_quantile(0.95, http_request_duration_bucket) // Latency p95
|
||||
```
|
||||
|
||||
**Health Check Endpoints:**
|
||||
| Endpoint | Purpose | Used By |
|
||||
|----------|---------|---------|
|
||||
| `/health/live` | Is process running? | K8s liveness probe |
|
||||
| `/health/ready` | Ready for traffic? | K8s readiness probe |
|
||||
| `/health` | Full status | Monitoring |
|
||||
|
||||
## Resources
|
||||
|
||||
- [OpenTelemetry](https://opentelemetry.io/docs/) - Distributed tracing standard
|
||||
- [Prometheus](https://prometheus.io/docs/) - Metrics and alerting
|
||||
- [Grafana](https://grafana.com/docs/) - Visualization
|
||||
- [Detailed Code Examples](./references/REFERENCE.md)
|
||||
- [Deployment Kubernetes](../deployment-kubernetes/SKILL.md) - K8s health probes
|
||||
- [Resilience Patterns](../resilience-patterns/SKILL.md) - Circuit breaker metrics
|
||||
@@ -1,457 +0,0 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
# Performance Optimization Patterns
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use this skill when:
|
||||
- Optimizing database queries
|
||||
- Detecting and fixing memory leaks
|
||||
- Profiling application performance
|
||||
- Optimizing connection pooling
|
||||
- Improving caching strategies
|
||||
- Identifyings N+1 query problems
|
||||
- Optimizing batch operations
|
||||
- Improving application startup time
|
||||
- Reducing memory usage
|
||||
- Optimizing API response times
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Performance Metrics
|
||||
|
||||
1. **Response Time**: Time to process request
|
||||
2. **Throughput**: Requests processed per second
|
||||
3. **Memory Usage**: Memory consumption
|
||||
4. **CPU Usage**: CPU utilization
|
||||
5. **Database Query Time**: Query execution time
|
||||
|
||||
### Optimization Areas
|
||||
|
||||
- Database queries
|
||||
- Memory management
|
||||
- Connection pooling
|
||||
- Caching
|
||||
- Batch processing
|
||||
- Lazy loading
|
||||
|
||||
## Database Query Optimization
|
||||
|
||||
### Query Analysis
|
||||
|
||||
```typescript
|
||||
// src/core/db/query-analyzer.ts
|
||||
// EN: Query performance analyzer
|
||||
// VI: Phân tích hiệu suất query
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { logger } from '@goodgo/logger';
|
||||
|
||||
export class QueryAnalyzer {
|
||||
constructor(private prisma: PrismaClient) {}
|
||||
|
||||
/**
|
||||
* EN: Analyze query performance
|
||||
* VI: Phân tích hiệu suất query
|
||||
*/
|
||||
async analyzeQuery(query: string): Promise<any> {
|
||||
const result = await this.prisma.$queryRawUnsafe(`EXPLAIN ANALYZE ${query}`);
|
||||
logger.info('Query analysis', { query, result });
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* EN: Check for missing indexes
|
||||
* VI: Kiểm tra indexes bị thiếu
|
||||
*/
|
||||
async checkIndexes(tableName: string): Promise<any> {
|
||||
const indexes = await this.prisma.$queryRawUnsafe(`
|
||||
SELECT * FROM pg_indexes WHERE tablename = '${tableName}'
|
||||
`);
|
||||
return indexes;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### N+1 Query Prevention
|
||||
|
||||
```typescript
|
||||
// EN: Bad: N+1 queries
|
||||
// VI: Xấu: N+1 queries
|
||||
async function getUsersWithOrdersBad(): Promise<User[]> {
|
||||
const users = await userRepository.findAll();
|
||||
for (const user of users) {
|
||||
user.orders = await orderRepository.findByUserId(user.id); // N+1!
|
||||
}
|
||||
return users;
|
||||
}
|
||||
|
||||
// EN: Good: Use include/join
|
||||
// VI: Tốt: Sử dụng include/join
|
||||
async function getUsersWithOrdersGood(): Promise<User[]> {
|
||||
return await userRepository.findAll({
|
||||
include: {
|
||||
orders: true, // EN: Single query with join / VI: Single query với join
|
||||
},
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Batch Operations
|
||||
|
||||
```typescript
|
||||
// src/core/db/batch-operations.ts
|
||||
// EN: Batch database operations
|
||||
// VI: Batch database operations
|
||||
export class BatchOperations {
|
||||
/**
|
||||
* EN: Batch create operations
|
||||
* VI: Batch create operations
|
||||
*/
|
||||
async batchCreate<T>(items: T[], batchSize: number = 100): Promise<T[]> {
|
||||
const results: T[] = [];
|
||||
|
||||
for (let i = 0; i < items.length; i += batchSize) {
|
||||
const batch = items.slice(i, i + batchSize);
|
||||
const batchResults = await prisma.$transaction(
|
||||
batch.map((item) => prisma.item.create({ data: item }))
|
||||
);
|
||||
results.push(...batchResults);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* EN: Batch update operations
|
||||
* VI: Batch update operations
|
||||
*/
|
||||
async batchUpdate(
|
||||
updates: Array<{ id: string; data: any }>,
|
||||
batchSize: number = 100
|
||||
): Promise<void> {
|
||||
for (let i = 0; i < updates.length; i += batchSize) {
|
||||
const batch = updates.slice(i, i + batchSize);
|
||||
await prisma.$transaction(
|
||||
batch.map(({ id, data }) => prisma.item.update({ where: { id }, data }))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Memory Leak Detection
|
||||
|
||||
### Memory Profiling
|
||||
|
||||
```typescript
|
||||
// src/core/performance/memory-profiler.ts
|
||||
// EN: Memory leak detection
|
||||
// VI: Phát hiện memory leak
|
||||
import { performance } from 'perf_hooks';
|
||||
import { logger } from '@goodgo/logger';
|
||||
|
||||
export class MemoryProfiler {
|
||||
private baseline?: NodeJS.MemoryUsage;
|
||||
private interval?: NodeJS.Timeout;
|
||||
|
||||
/**
|
||||
* EN: Start memory profiling
|
||||
* VI: Bắt đầu memory profiling
|
||||
*/
|
||||
start(): void {
|
||||
this.baseline = process.memoryUsage();
|
||||
|
||||
this.interval = setInterval(() => {
|
||||
const current = process.memoryUsage();
|
||||
const delta = {
|
||||
heapUsed: current.heapUsed - (this.baseline?.heapUsed || 0),
|
||||
heapTotal: current.heapTotal - (this.baseline?.heapTotal || 0),
|
||||
external: current.external - (this.baseline?.external || 0),
|
||||
};
|
||||
|
||||
logger.info('Memory usage', {
|
||||
current: {
|
||||
heapUsed: `${(current.heapUsed / 1024 / 1024).toFixed(2)} MB`,
|
||||
heapTotal: `${(current.heapTotal / 1024 / 1024).toFixed(2)} MB`,
|
||||
},
|
||||
delta: {
|
||||
heapUsed: `${(delta.heapUsed / 1024 / 1024).toFixed(2)} MB`,
|
||||
},
|
||||
});
|
||||
|
||||
// EN: Alert if memory growth is excessive
|
||||
// VI: Cảnh báo nếu memory tăng quá mức
|
||||
if (delta.heapUsed > 100 * 1024 * 1024) {
|
||||
// 100MB growth
|
||||
logger.warn('Potential memory leak detected', { delta });
|
||||
}
|
||||
}, 60000); // EN: Check every minute / VI: Kiểm tra mỗi phút
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
if (this.interval) {
|
||||
clearInterval(this.interval);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* EN: Force garbage collection (if --expose-gc flag is set)
|
||||
* VI: Ép garbage collection (nếu --expose-gc flag được set)
|
||||
*/
|
||||
forceGC(): void {
|
||||
if (global.gc) {
|
||||
global.gc();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Connection Pooling
|
||||
|
||||
### Database Connection Pool
|
||||
|
||||
```typescript
|
||||
// src/config/database.config.ts
|
||||
// EN: Optimize Prisma connection pool
|
||||
// VI: Tối ưu Prisma connection pool
|
||||
export const prisma = new PrismaClient({
|
||||
datasources: {
|
||||
db: {
|
||||
url: process.env.DATABASE_URL,
|
||||
},
|
||||
},
|
||||
log: process.env.NODE_ENV === 'development' ? ['query', 'error'] : ['error'],
|
||||
});
|
||||
|
||||
// EN: Connection pool configuration in DATABASE_URL
|
||||
// VI: Cấu hình connection pool trong DATABASE_URL
|
||||
// DATABASE_URL="postgresql://user:pass@host:5432/db?connection_limit=20&pool_timeout=10"
|
||||
```
|
||||
|
||||
### HTTP Connection Pool
|
||||
|
||||
```typescript
|
||||
// src/core/clients/http-pool.config.ts
|
||||
// EN: HTTP connection pooling with keep-alive
|
||||
// VI: HTTP connection pooling với keep-alive
|
||||
import axios from 'axios';
|
||||
import https from 'https';
|
||||
|
||||
const httpAgent = new https.Agent({
|
||||
keepAlive: true,
|
||||
keepAliveMsecs: 1000,
|
||||
maxSockets: 50,
|
||||
maxFreeSockets: 10,
|
||||
timeout: 60000,
|
||||
});
|
||||
|
||||
export const httpClient = axios.create({
|
||||
httpAgent,
|
||||
httpsAgent: httpAgent,
|
||||
timeout: 30000,
|
||||
});
|
||||
```
|
||||
|
||||
## Performance Monitoring
|
||||
|
||||
```typescript
|
||||
// src/core/performance/performance-monitor.ts
|
||||
// EN: Performance monitoring middleware
|
||||
// VI: Middleware giám sát hiệu suất
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { performance } from 'perf_hooks';
|
||||
import { logger } from '@goodgo/logger';
|
||||
|
||||
export function performanceMonitor(req: Request, res: Response, next: NextFunction) {
|
||||
const start = performance.now();
|
||||
const startMemory = process.memoryUsage();
|
||||
|
||||
res.on('finish', () => {
|
||||
const duration = performance.now() - start;
|
||||
const endMemory = process.memoryUsage();
|
||||
|
||||
logger.info('Request performance', {
|
||||
method: req.method,
|
||||
path: req.path,
|
||||
status: res.statusCode,
|
||||
duration: `${duration.toFixed(2)}ms`,
|
||||
memoryDelta: `${((endMemory.heapUsed - startMemory.heapUsed) / 1024 / 1024).toFixed(2)} MB`,
|
||||
});
|
||||
|
||||
// EN: Alert on slow requests
|
||||
// VI: Cảnh báo trên requests chậm
|
||||
if (duration > 1000) {
|
||||
logger.warn('Slow request detected', {
|
||||
path: req.path,
|
||||
duration: `${duration.toFixed(2)}ms`,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
next();
|
||||
}
|
||||
```
|
||||
|
||||
## Caching Optimization
|
||||
|
||||
```typescript
|
||||
// src/core/cache/cache-optimizer.ts
|
||||
// EN: Cache optimization strategies
|
||||
// VI: Chiến lược tối ưu cache
|
||||
export class CacheOptimizer {
|
||||
/**
|
||||
* EN: Preload frequently accessed data
|
||||
* VI: Tải trước dữ liệu thường truy cập
|
||||
*/
|
||||
async preloadHotData(keys: string[]): Promise<void> {
|
||||
for (const key of keys) {
|
||||
// EN: Fetch and cache data
|
||||
// VI: Fetch và cache dữ liệu
|
||||
await cacheService.getOrSet(key, async () => {
|
||||
return await this.fetchData(key);
|
||||
}, 3600);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* EN: Batch cache operations
|
||||
* VI: Batch cache operations
|
||||
*/
|
||||
async batchGet(keys: string[]): Promise<Map<string, any>> {
|
||||
const results = new Map<string, any>();
|
||||
const missing: string[] = [];
|
||||
|
||||
// EN: Check cache for all keys
|
||||
// VI: Kiểm tra cache cho tất cả keys
|
||||
for (const key of keys) {
|
||||
const cached = await cacheService.get(key);
|
||||
if (cached) {
|
||||
results.set(key, cached);
|
||||
} else {
|
||||
missing.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
// EN: Fetch missing data
|
||||
// VI: Fetch dữ liệu thiếu
|
||||
if (missing.length > 0) {
|
||||
const data = await this.fetchBatch(missing);
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
results.set(key, value);
|
||||
await cacheService.set(key, value, 3600);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Database**: Use indexes, avoid N+1 queries, use batch operations
|
||||
2. **Memory**: Monitor memory usage, detect leaks, use streaming for large data
|
||||
3. **Caching**: Cache frequently accessed data, use appropriate TTLs
|
||||
4. **Connection Pooling**: Configure pool sizes appropriately
|
||||
5. **Profiling**: Profile regularly to identify bottlenecks
|
||||
6. **Monitoring**: Monitor performance metrics continuously
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
1. **N+1 Queries**: Fetching related data in loops
|
||||
```typescript
|
||||
// ❌ BAD: N+1
|
||||
for (const user of users) {
|
||||
user.orders = await orderRepo.findByUserId(user.id);
|
||||
}
|
||||
|
||||
// ✅ GOOD: Use include
|
||||
const users = await userRepo.findAll({ include: { orders: true } });
|
||||
```
|
||||
|
||||
2. **No Connection Pooling**: Creating new connections per request
|
||||
```typescript
|
||||
// ❌ BAD: No pooling
|
||||
const client = new PrismaClient(); // per request
|
||||
|
||||
// ✅ GOOD: Reuse singleton
|
||||
import { prisma } from './db'; // shared instance
|
||||
```
|
||||
|
||||
3. **Missing Indexes**: Slow queries on frequently searched columns
|
||||
```prisma
|
||||
// ✅ Add indexes in schema
|
||||
@@index([createdAt])
|
||||
@@index([userId, status])
|
||||
```
|
||||
|
||||
4. **Unbounded Queries**: Fetching all data without limits
|
||||
```typescript
|
||||
// ❌ BAD
|
||||
const all = await repo.findAll();
|
||||
|
||||
// ✅ GOOD
|
||||
const page = await repo.findAll({ take: 100, skip: offset });
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Metric | Target | Alert Threshold |
|
||||
|--------|--------|-----------------|
|
||||
| **Response Time (p95)** | <200ms | >500ms |
|
||||
| **Memory Usage** | <70% | >85% |
|
||||
| **CPU Usage** | <60% | >80% |
|
||||
| **Query Time** | <50ms | >200ms |
|
||||
|
||||
**Profiling Commands:**
|
||||
```bash
|
||||
# CPU profiling
|
||||
node --prof app.js
|
||||
node --prof-process isolate-*.log > profile.txt
|
||||
|
||||
# Memory snapshot
|
||||
node --inspect app.js # Use Chrome DevTools
|
||||
|
||||
# Clinic.js
|
||||
npx clinic doctor -- node app.js
|
||||
npx clinic flame -- node app.js
|
||||
```
|
||||
|
||||
**Prisma Query Optimization:**
|
||||
```typescript
|
||||
// Select only needed fields
|
||||
await prisma.user.findMany({ select: { id: true, email: true } });
|
||||
|
||||
// Use include for relations
|
||||
await prisma.user.findMany({ include: { posts: true } });
|
||||
|
||||
// Batch operations
|
||||
await prisma.$transaction([...operations]);
|
||||
|
||||
// Raw query for complex cases
|
||||
await prisma.$queryRaw`SELECT ... FROM ...`;
|
||||
```
|
||||
|
||||
**Connection Pool Config:**
|
||||
```bash
|
||||
# In DATABASE_URL
|
||||
?connection_limit=20&pool_timeout=10
|
||||
```
|
||||
|
||||
**Performance Checklist:**
|
||||
- [ ] Add database indexes for frequent queries
|
||||
- [ ] Use pagination for list endpoints
|
||||
- [ ] Implement caching for hot data
|
||||
- [ ] Enable connection pooling
|
||||
- [ ] Profile and monitor regularly
|
||||
- [ ] Use batch operations for bulk updates
|
||||
|
||||
## Resources
|
||||
|
||||
- [Node.js Performance](https://nodejs.org/en/docs/guides/simple-profiling/)
|
||||
- [Prisma Performance](https://www.prisma.io/docs/guides/performance-and-optimization)
|
||||
- [Caching Patterns](../caching-patterns/SKILL.md) - Caching strategies
|
||||
- [Observability & Monitoring](../observability-monitoring/SKILL.md) - Monitoring patterns
|
||||
- [Database & Prisma](../database-prisma/SKILL.md) - Database patterns
|
||||
@@ -1,329 +0,0 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
# GoodGo Project Rules
|
||||
|
||||
## Architecture
|
||||
|
||||
**Monorepo Structure:**
|
||||
- **Apps**: Next.js (web) + Flutter (mobile)
|
||||
- **Services**: Node.js/TypeScript microservices (Express)
|
||||
- **Packages**: Shared libraries (logger, types, http-client, auth-sdk, tracing)
|
||||
- **Infrastructure**: Traefik (API Gateway), Redis, Neon PostgreSQL, Observability
|
||||
- **Deployments**: Local (Docker Compose), Staging/Production (Kubernetes)
|
||||
|
||||
**Template Location**: `services/_template/` - Use as starting point for new services
|
||||
|
||||
## Tech Stack
|
||||
|
||||
**Frontend:**
|
||||
- Next.js 14+ (App Router), TypeScript, Tailwind CSS, Zustand
|
||||
- Flutter 3.x with Provider pattern
|
||||
- Use `@goodgo/types` and `@goodgo/http-client`
|
||||
|
||||
**Backend:**
|
||||
- Node.js 20+, TypeScript 5+, Express
|
||||
- Prisma ORM + Neon PostgreSQL
|
||||
- Zod validation, `@goodgo/logger`, `@goodgo/tracing`, `@goodgo/auth-sdk`
|
||||
|
||||
**Infrastructure:**
|
||||
- Traefik (path-based routing), Redis (cache), Prometheus + Grafana + Loki
|
||||
|
||||
## Project Structure
|
||||
|
||||
**Service:** `src/{config,modules,middlewares,routes,main.ts}` + `prisma/` + `Dockerfile`
|
||||
**Package:** `src/index.ts` + `package.json` + `tsconfig.json` + `README.md`
|
||||
**App:** `src/{app,services/api,stores}` + `Dockerfile`
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
- **Services/Packages**: `kebab-case` (e.g., `auth-service`, `http-client`)
|
||||
- **Files**: `kebab-case.type.ts` (e.g., `user.controller.ts`)
|
||||
- **Components**: `PascalCase.tsx` (React), `snake_case.dart` (Flutter)
|
||||
- **Classes**: `PascalCase`, **Functions**: `camelCase`, **Constants**: `UPPER_SNAKE_CASE`
|
||||
- **Package Names**: `@goodgo/package-name`
|
||||
|
||||
## Workflows
|
||||
|
||||
**New Service:**
|
||||
1. Copy `services/_template/`
|
||||
2. Update `package.json` name to `@goodgo/service-name`
|
||||
3. Add to `deployments/local/docker-compose.yml` with Traefik labels
|
||||
4. Configure Prisma schema if needed
|
||||
5. Add health check endpoint
|
||||
|
||||
**New Package:**
|
||||
1. Create in `packages/`, export from `src/index.ts`
|
||||
2. Add to `pnpm-workspace.yaml`
|
||||
3. Use TypeScript strict mode
|
||||
|
||||
**Dependencies:**
|
||||
```bash
|
||||
pnpm --filter @goodgo/service-name add package-name
|
||||
pnpm --filter @goodgo/service-name add @goodgo/logger # workspace
|
||||
pnpm --filter @goodgo/service-name add -D @types/pkg # dev
|
||||
```
|
||||
|
||||
**Database:**
|
||||
```bash
|
||||
pnpm --filter @goodgo/service-name prisma migrate dev
|
||||
pnpm --filter @goodgo/service-name prisma generate
|
||||
```
|
||||
|
||||
## Code Standards
|
||||
|
||||
**TypeScript:**
|
||||
- Strict mode, no `any` (use `unknown`)
|
||||
- Zod for runtime validation
|
||||
- Export shared types from `@goodgo/types`
|
||||
|
||||
**API Responses:**
|
||||
```typescript
|
||||
// Success: { success: true, data: any }
|
||||
// Error: { success: false, error: { code, message, details? } }
|
||||
```
|
||||
|
||||
**Logging:**
|
||||
```typescript
|
||||
import { logger } from '@goodgo/logger';
|
||||
logger.info('Message', { context });
|
||||
logger.error('Error', { error, context });
|
||||
```
|
||||
|
||||
**Environment:**
|
||||
- Use `.env.example` template, never commit `.env`
|
||||
- Validate with Zod at startup
|
||||
- Document all vars in README
|
||||
|
||||
## Testing
|
||||
|
||||
- **Unit**: Place tests next to source (`*.test.ts`), use Jest, mock dependencies, >80% coverage
|
||||
- **Integration**: Test API endpoints, use test database, cleanup after
|
||||
- **Commands**: `pnpm test`, `pnpm --filter @goodgo/service-name test`, `pnpm test:coverage`
|
||||
|
||||
## Docker
|
||||
|
||||
**Multi-stage Build Pattern:**
|
||||
```dockerfile
|
||||
FROM node:20-alpine AS builder
|
||||
# ... build stage
|
||||
FROM node:20-alpine
|
||||
# ... production stage with non-root user
|
||||
```
|
||||
|
||||
**Image Naming:** `goodgo/service-name:version` (semantic versioning)
|
||||
|
||||
## Git Workflow
|
||||
|
||||
**Branches:** `feature/`, `fix/`, `hotfix/`, `release/`
|
||||
|
||||
**Commits:** Conventional Commits format
|
||||
```
|
||||
type(scope): subject
|
||||
```
|
||||
Types: `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore`
|
||||
|
||||
**PRs:** Use template, link issues, ensure CI passes, squash merge to main
|
||||
|
||||
## CI/CD
|
||||
|
||||
**GitHub Actions:** PR (lint, test, build) → `develop` (staging) → `main` (production)
|
||||
|
||||
**Deployment Checklist:** Tests pass, no lint errors, env vars set, migrations applied, docs updated, monitoring configured
|
||||
|
||||
## Security
|
||||
|
||||
**Auth:** JWT (15min access, 7d refresh), httpOnly cookies, use `@goodgo/auth-sdk`
|
||||
**Authorization:** RBAC, check permissions at service level, middleware for routes
|
||||
**Data:** bcrypt (cost 12), HTTPS, sanitize inputs, Zod validation
|
||||
**Secrets:** Environment variables, Kubernetes secrets, never hardcode, rotate regularly
|
||||
|
||||
## Performance
|
||||
|
||||
**Backend:** Redis caching, connection pooling, pagination, database indexes, rate limiting
|
||||
**Frontend:** Next.js Image optimization, code splitting, lazy loading, React.memo, bundle optimization
|
||||
**Database:** Prisma optimization, indexes, transactions, soft deletes
|
||||
|
||||
## Observability
|
||||
|
||||
**Metrics:** Prometheus (request count, duration, errors), set alerts
|
||||
**Logging:** `@goodgo/logger` with trace IDs, levels (error, warn, info, debug), Loki aggregation
|
||||
**Tracing:** OpenTelemetry via `@goodgo/tracing`, trace cross-service requests
|
||||
|
||||
## Documentation
|
||||
|
||||
**Code:** JSDoc for public APIs, inline comments for complex logic, README per service/package
|
||||
**API:** OpenAPI/Swagger specs in `docs/api/openapi/`, document endpoints with examples
|
||||
**Architecture:** System design in `docs/architecture/`, service communication, data flows, ADRs
|
||||
|
||||
## Architecture Patterns
|
||||
|
||||
**Modular Structure:** Controller → Service → Repository pattern
|
||||
**DTO Validation:** Zod schemas with type inference
|
||||
**Error Handling:** Custom error classes, global error middleware
|
||||
**Dependency Injection:** Constructor injection for testability
|
||||
|
||||
**Example Module:**
|
||||
```typescript
|
||||
// DTO with Zod
|
||||
export const CreateFeatureDto = z.object({
|
||||
name: z.string().min(1),
|
||||
email: z.string().email()
|
||||
});
|
||||
export type CreateFeatureDto = z.infer<typeof CreateFeatureDto>;
|
||||
|
||||
// Controller
|
||||
export class FeatureController {
|
||||
constructor(private service: FeatureService) {}
|
||||
async create(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const dto = CreateFeatureDto.parse(req.body);
|
||||
const result = await this.service.create(dto);
|
||||
res.json({ success: true, data: result });
|
||||
} catch (error) { next(error); }
|
||||
}
|
||||
}
|
||||
|
||||
// Service
|
||||
export class FeatureService {
|
||||
constructor(private repository: FeatureRepository) {}
|
||||
async create(dto: CreateFeatureDto) {
|
||||
return this.repository.create(dto);
|
||||
}
|
||||
}
|
||||
|
||||
// Repository
|
||||
export class FeatureRepository extends BaseRepository<Feature> {
|
||||
async create(data: CreateFeatureDto) {
|
||||
return this.prisma.feature.create({ data });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Deployment & Traefik
|
||||
|
||||
**Service Registration:**
|
||||
Services are deployed via `deployments/local/docker-compose.yml` and auto-discovered by Traefik:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
my-service:
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: services/my-service/Dockerfile
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.my-service.rule=PathPrefix(`/api/v1/my-service`)"
|
||||
- "traefik.http.services.my-service.loadbalancer.server.port=5002"
|
||||
- "traefik.http.services.my-service.loadbalancer.healthcheck.path=/health/live"
|
||||
```
|
||||
|
||||
**Traefik Configuration:**
|
||||
- **Location**: `infra/traefik/` (platform-level, not per-service)
|
||||
- **Static Config**: `traefik.yml` - Entry points, providers, dashboard
|
||||
- **Dynamic Config**: `dynamic/middlewares.yml`, `dynamic/routes.yml`
|
||||
- **Dashboard**: http://localhost:8080
|
||||
|
||||
**Access Points:**
|
||||
- API: `http://localhost/api/v1/service-name`
|
||||
- Health: `http://localhost/api/v1/service-name/health`
|
||||
- Docs: `http://localhost/api/v1/service-name/api-docs`
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Common Issues:**
|
||||
- Port conflicts: Check `deployments/local/docker-compose.yml`
|
||||
- Database: Verify `DATABASE_URL` in `.env.local`
|
||||
- Module not found: Run `pnpm install`
|
||||
- Type errors: Run `pnpm --filter @goodgo/service-name prisma generate`
|
||||
|
||||
**Debug:**
|
||||
```bash
|
||||
cd deployments/local
|
||||
docker-compose logs -f service-name
|
||||
docker-compose ps
|
||||
docker-compose up -d --build
|
||||
```
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
1. **Wrong Directory Structure**: Placing files in wrong locations
|
||||
```
|
||||
# ❌ BAD: Controllers outside modules
|
||||
src/controllers/user.controller.ts
|
||||
|
||||
# ✅ GOOD: Controllers inside modules
|
||||
src/modules/user/user.controller.ts
|
||||
```
|
||||
|
||||
2. **Inconsistent Naming**: Mixing naming conventions
|
||||
```typescript
|
||||
// ❌ BAD: Mixed naming
|
||||
UserService.ts
|
||||
user_repository.ts
|
||||
userController.ts
|
||||
|
||||
// ✅ GOOD: Consistent kebab-case
|
||||
user.service.ts
|
||||
user.repository.ts
|
||||
user.controller.ts
|
||||
```
|
||||
|
||||
3. **Wrong Response Format**: Not following API response standard
|
||||
```typescript
|
||||
// ❌ BAD: Inconsistent format
|
||||
res.json({ user: data });
|
||||
res.json({ error: "Not found" });
|
||||
|
||||
// ✅ GOOD: Standard format
|
||||
res.json({ success: true, data: user });
|
||||
res.json({ success: false, error: { code: 'NOT_FOUND', message: 'User not found' } });
|
||||
```
|
||||
|
||||
4. **Missing Bilingual Comments**: Only one language
|
||||
```typescript
|
||||
// ❌ BAD: English only
|
||||
// Initialize database
|
||||
|
||||
// ✅ GOOD: Bilingual
|
||||
// EN: Initialize database connection
|
||||
// VI: Khởi tạo kết nối database
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Category | Pattern/Standard |
|
||||
|----------|-----------------|
|
||||
| **Service structure** | `src/{config,modules,middlewares,routes,main.ts}` |
|
||||
| **File naming** | `kebab-case.type.ts` (e.g., `user.controller.ts`) |
|
||||
| **Package naming** | `@goodgo/package-name` |
|
||||
| **API response** | `{ success: true, data }` / `{ success: false, error: { code, message } }` |
|
||||
| **Password hashing** | bcrypt, cost 12 |
|
||||
| **JWT tokens** | Access: 15min, Refresh: 7 days |
|
||||
| **Coverage target** | >80% for unit tests |
|
||||
| **Commits** | `type(scope): subject` (conventional commits) |
|
||||
|
||||
**Common Commands:**
|
||||
```bash
|
||||
# Add dependency
|
||||
pnpm --filter @goodgo/service-name add package-name
|
||||
|
||||
# Run migrations
|
||||
pnpm --filter @goodgo/service-name prisma migrate dev
|
||||
|
||||
# Run tests
|
||||
pnpm --filter @goodgo/service-name test
|
||||
|
||||
# Start dev server
|
||||
pnpm --filter @goodgo/service-name dev
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
- [Architecture Docs](../../docs/architecture/)
|
||||
- [API Specs](../../docs/api/openapi/)
|
||||
- [Development Guide](../../docs/guides/development.md)
|
||||
- [Deployment Guide](../../docs/guides/deployment.md)
|
||||
- [Neon Database Guide](../../docs/guides/neon-database.md)
|
||||
- [Contributing Guide](../../CONTRIBUTING.md)
|
||||
@@ -1,273 +0,0 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
# Repository Pattern
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use this skill when:
|
||||
- Implementing data access layers for new modules
|
||||
- Extending BaseRepository for specific entity types
|
||||
- Writing custom database queries
|
||||
- Handling database transactions
|
||||
- Optimizing database queries and operations
|
||||
- Testing repository implementations
|
||||
- Organizing data access code
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Repository Pattern Benefits
|
||||
|
||||
1. **Abstraction**: Separates business logic from data access
|
||||
2. **Testability**: Easy to mock repositories for testing
|
||||
3. **Maintainability**: Centralized database operations
|
||||
4. **Consistency**: Standardized CRUD operations
|
||||
5. **Type Safety**: TypeScript generics provide type safety
|
||||
|
||||
### BaseRepository Class
|
||||
|
||||
The `BaseRepository` abstract class provides common database operations that can be extended:
|
||||
|
||||
- `findById(id)` - Find entity by ID
|
||||
- `findByUnique(field, value)` - Find by unique field
|
||||
- `findAll(options)` - Find all with filtering, pagination, sorting
|
||||
- `create(data)` - Create new entity
|
||||
- `update(id, data)` - Update entity
|
||||
- `delete(id)` - Delete entity
|
||||
- `count(where)` - Count entities
|
||||
- `exists(id)` - Check if entity exists
|
||||
- `transaction(callback)` - Execute transaction
|
||||
|
||||
## Patterns
|
||||
|
||||
### Extending BaseRepository
|
||||
|
||||
```typescript
|
||||
import { PrismaClient, User } from '@prisma/client';
|
||||
import { BaseRepository } from '../modules/common/repository';
|
||||
|
||||
export class UserRepository extends BaseRepository<User, CreateUserInput, UpdateUserInput> {
|
||||
constructor(prisma: PrismaClient) {
|
||||
super(prisma, 'user');
|
||||
}
|
||||
|
||||
// Add custom methods
|
||||
async findByEmail(email: string): Promise<User | null> {
|
||||
return this.prisma.user.findUnique({
|
||||
where: { email },
|
||||
});
|
||||
}
|
||||
|
||||
async findByUsername(username: string): Promise<User | null> {
|
||||
return this.prisma.user.findUnique({
|
||||
where: { username },
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Query Methods
|
||||
|
||||
Add domain-specific query methods:
|
||||
|
||||
```typescript
|
||||
export class UserRepository extends BaseRepository<User, CreateUserInput, UpdateUserInput> {
|
||||
// Find user with related data
|
||||
async findWithPermissions(userId: string): Promise<User | null> {
|
||||
return this.prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
include: {
|
||||
userRoles: {
|
||||
include: { role: true },
|
||||
},
|
||||
userPermissions: {
|
||||
include: { permission: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Complex query with filtering
|
||||
async findActiveUsers(organizationId?: string): Promise<User[]> {
|
||||
return this.prisma.user.findMany({
|
||||
where: {
|
||||
isActive: true,
|
||||
...(organizationId && { organizationId }),
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Using Repository Interface
|
||||
|
||||
Implement the `IRepository` interface for type safety:
|
||||
|
||||
```typescript
|
||||
import { IRepository } from '../modules/common/repository';
|
||||
|
||||
export class UserRepository
|
||||
extends BaseRepository<User, CreateUserInput, UpdateUserInput>
|
||||
implements IRepository<User, CreateUserInput, UpdateUserInput> {
|
||||
|
||||
// Implementation...
|
||||
}
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
BaseRepository handles errors automatically:
|
||||
|
||||
```typescript
|
||||
async findById(id: string): Promise<T | null> {
|
||||
try {
|
||||
// Database operation
|
||||
const entity = await this.prisma.user.findUnique({ where: { id } });
|
||||
return entity;
|
||||
} catch (error: any) {
|
||||
logger.error(`Failed to find ${this.modelName} by ID`, { error, id });
|
||||
throw new DatabaseError(`Failed to find ${this.modelName}`, { id, originalError: error });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Transactions
|
||||
|
||||
Use repository transaction method for multiple operations:
|
||||
|
||||
```typescript
|
||||
await repository.transaction(async (tx) => {
|
||||
const user = await tx.user.create({ data: userData });
|
||||
await tx.userProfile.create({ data: { userId: user.id, ...profileData } });
|
||||
return user;
|
||||
});
|
||||
```
|
||||
|
||||
### Query Options
|
||||
|
||||
Use Prisma query options in findAll:
|
||||
|
||||
```typescript
|
||||
// Pagination
|
||||
const users = await userRepository.findAll({
|
||||
skip: (page - 1) * limit,
|
||||
take: limit,
|
||||
});
|
||||
|
||||
// Filtering
|
||||
const activeUsers = await userRepository.findAll({
|
||||
where: { isActive: true },
|
||||
});
|
||||
|
||||
// Sorting
|
||||
const recentUsers = await userRepository.findAll({
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
|
||||
// Including relations
|
||||
const usersWithRoles = await userRepository.findAll({
|
||||
include: { userRoles: true },
|
||||
});
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Extend BaseRepository**: Always extend BaseRepository instead of implementing from scratch
|
||||
2. **Custom Methods**: Add domain-specific query methods in repository subclasses
|
||||
3. **Type Safety**: Use TypeScript generics for type safety
|
||||
4. **Error Handling**: Let BaseRepository handle common errors, handle domain-specific errors in custom methods
|
||||
5. **Logging**: BaseRepository handles logging automatically
|
||||
6. **Transactions**: Use repository transaction method for multi-step operations
|
||||
7. **Query Optimization**: Use Prisma query options (select, include) to optimize queries
|
||||
8. **Single Responsibility**: Each repository handles one entity type
|
||||
9. **Dependency Injection**: Inject PrismaClient in constructor for testability
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
1. **Not Extending BaseRepository**: Implementing CRUD from scratch instead of extending
|
||||
2. **Business Logic in Repository**: Putting business logic in repository instead of service layer
|
||||
3. **Exposing Prisma Client**: Exposing raw Prisma client instead of using repository methods
|
||||
4. **Missing Error Handling**: Not handling errors in custom query methods
|
||||
5. **Over-fetching Data**: Using `include` unnecessarily, fetching too much data
|
||||
6. **No Type Safety**: Not using TypeScript generics properly
|
||||
7. **Transaction Mistakes**: Not using repository transaction method for related operations
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Type Errors with Prisma
|
||||
|
||||
**Problem**: TypeScript errors when using Prisma client methods
|
||||
**Solution**: Ensure Prisma client is generated: `pnpm prisma generate`. Use proper type assertions if needed.
|
||||
|
||||
### Transaction Rollback Issues
|
||||
|
||||
**Problem**: Transaction not rolling back on error
|
||||
**Solution**: Ensure all operations in transaction callback use the transaction client (`tx`) parameter, not the main Prisma client.
|
||||
|
||||
### Performance Issues
|
||||
|
||||
**Problem**: Slow queries or N+1 query problems
|
||||
**Solution**: Use `include` to fetch related data in single query. Use `select` to limit fields. Add database indexes.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
**BaseRepository Methods:**
|
||||
| Method | Returns | Description |
|
||||
|--------|---------|-------------|
|
||||
| `findById(id)` | `T \| null` | Find by primary key |
|
||||
| `findByUnique(field, value)` | `T \| null` | Find by unique field |
|
||||
| `findAll(options)` | `T[]` | Find with filters |
|
||||
| `create(data)` | `T` | Create entity |
|
||||
| `update(id, data)` | `T` | Update entity |
|
||||
| `delete(id)` | `void` | Delete entity |
|
||||
| `count(where)` | `number` | Count entities |
|
||||
| `exists(id)` | `boolean` | Check existence |
|
||||
| `transaction(cb)` | `R` | Execute transaction |
|
||||
|
||||
**Query Options:**
|
||||
```typescript
|
||||
// Pagination
|
||||
{ skip: 0, take: 10 }
|
||||
|
||||
// Filtering
|
||||
{ where: { isActive: true } }
|
||||
|
||||
// Sorting
|
||||
{ orderBy: { createdAt: 'desc' } }
|
||||
|
||||
// Relations
|
||||
{ include: { roles: true } }
|
||||
|
||||
// Field selection
|
||||
{ select: { id: true, email: true } }
|
||||
```
|
||||
|
||||
**Repository Template:**
|
||||
```typescript
|
||||
export class EntityRepository extends BaseRepository<Entity, CreateDto, UpdateDto> {
|
||||
constructor(prisma: PrismaClient) {
|
||||
super(prisma, 'entity');
|
||||
}
|
||||
|
||||
// Custom query methods
|
||||
async findByField(value: string): Promise<Entity | null> {
|
||||
return this.prisma.entity.findUnique({ where: { field: value } });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Essential Imports:**
|
||||
```typescript
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { BaseRepository, IRepository } from '../modules/common/repository';
|
||||
import { DatabaseError } from '../errors/http-error';
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
- [BaseRepository](../../services/iam-service/src/modules/common/repository.ts) - Base repository implementation
|
||||
- [User Repository](../../services/iam-service/src/repositories/user.repository.ts) - Example repository
|
||||
- [Database Prisma](../database-prisma/SKILL.md) - Prisma ORM patterns
|
||||
- [Error Handling](../error-handling-patterns/SKILL.md) - Error handling in repositories
|
||||
@@ -1,421 +0,0 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
# Resilience Patterns
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use this skill when:
|
||||
- Implementing circuit breaker patterns for external services
|
||||
- Adding retry logic for transient failures
|
||||
- Setting timeout handling for long-running operations
|
||||
- Implementing graceful degradation strategies
|
||||
- Handling external service failures
|
||||
- Improving system fault tolerance
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Resilience Patterns
|
||||
|
||||
1. **Circuit Breaker**: Prevents cascading failures by stopping calls to failing services
|
||||
2. **Retry**: Automatically retries failed operations with backoff
|
||||
3. **Timeout**: Sets maximum time limits for operations
|
||||
4. **Bulkhead**: Isolates failures to prevent spread
|
||||
5. **Graceful Degradation**: Provides fallback behavior when services fail
|
||||
|
||||
## Patterns
|
||||
|
||||
### Circuit Breaker Pattern
|
||||
|
||||
Protects against cascading failures:
|
||||
|
||||
```typescript
|
||||
import CircuitBreaker from 'opossum';
|
||||
import { logger } from '@goodgo/logger';
|
||||
|
||||
export const createCircuitBreaker = <TArgs extends any[], TResult>(
|
||||
action: (...args: TArgs) => Promise<TResult>,
|
||||
name: string,
|
||||
options: Partial<CircuitBreaker.Options> = {}
|
||||
): CircuitBreaker<TArgs, TResult> => {
|
||||
const breaker = new CircuitBreaker(action, {
|
||||
timeout: 3000,
|
||||
errorThresholdPercentage: 50,
|
||||
resetTimeout: 30000,
|
||||
...options,
|
||||
name,
|
||||
});
|
||||
|
||||
breaker.on('open', () => {
|
||||
logger.warn(`Circuit Breaker OPEN: ${name}`);
|
||||
});
|
||||
|
||||
breaker.on('halfOpen', () => {
|
||||
logger.info(`Circuit Breaker HALF-OPEN: ${name}`);
|
||||
});
|
||||
|
||||
breaker.on('close', () => {
|
||||
logger.info(`Circuit Breaker CLOSED: ${name}`);
|
||||
});
|
||||
|
||||
return breaker;
|
||||
};
|
||||
|
||||
// Usage
|
||||
const externalApiBreaker = createCircuitBreaker(
|
||||
async (data) => await externalApi.call(data),
|
||||
'external-api'
|
||||
);
|
||||
|
||||
try {
|
||||
const result = await externalApiBreaker.fire(requestData);
|
||||
} catch (error) {
|
||||
// Handle circuit breaker error or fallback
|
||||
}
|
||||
```
|
||||
|
||||
### Retry Pattern
|
||||
|
||||
Retry transient failures with exponential backoff:
|
||||
|
||||
```typescript
|
||||
async function retryWithBackoff<T>(
|
||||
fn: () => Promise<T>,
|
||||
maxRetries: number = 3,
|
||||
baseDelay: number = 1000
|
||||
): Promise<T> {
|
||||
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (error) {
|
||||
if (attempt === maxRetries) throw error;
|
||||
|
||||
const delay = baseDelay * Math.pow(2, attempt);
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
}
|
||||
}
|
||||
throw new Error('Retry exhausted');
|
||||
}
|
||||
```
|
||||
|
||||
### Timeout Pattern
|
||||
|
||||
Set maximum time limits:
|
||||
|
||||
```typescript
|
||||
async function withTimeout<T>(
|
||||
promise: Promise<T>,
|
||||
timeoutMs: number
|
||||
): Promise<T> {
|
||||
const timeout = new Promise<never>((_, reject) => {
|
||||
setTimeout(() => reject(new Error('Operation timeout')), timeoutMs);
|
||||
});
|
||||
|
||||
return Promise.race([promise, timeout]);
|
||||
}
|
||||
|
||||
// Usage
|
||||
try {
|
||||
const result = await withTimeout(
|
||||
externalService.call(),
|
||||
5000 // 5 second timeout
|
||||
);
|
||||
} catch (error) {
|
||||
if (error.message === 'Operation timeout') {
|
||||
// Handle timeout
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Graceful Degradation
|
||||
|
||||
Provide fallback behavior:
|
||||
|
||||
```typescript
|
||||
async function getDataWithFallback() {
|
||||
try {
|
||||
return await primaryDataSource.get();
|
||||
} catch (error) {
|
||||
logger.warn('Primary source failed, using fallback', { error });
|
||||
return await fallbackDataSource.get();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Bulkhead Pattern
|
||||
|
||||
Isolate failures to prevent spread:
|
||||
|
||||
```typescript
|
||||
import PQueue from 'p-queue';
|
||||
|
||||
// Create separate queues for different operations
|
||||
const externalApiQueue = new PQueue({
|
||||
concurrency: 10, // Max 10 concurrent calls
|
||||
timeout: 30000 // 30 second timeout per operation
|
||||
});
|
||||
|
||||
const databaseQueue = new PQueue({
|
||||
concurrency: 20
|
||||
});
|
||||
|
||||
// Usage - operations are isolated
|
||||
async function fetchExternalData(id: string) {
|
||||
return externalApiQueue.add(async () => {
|
||||
return await externalApi.getData(id);
|
||||
});
|
||||
}
|
||||
|
||||
async function queryDatabase(query: string) {
|
||||
return databaseQueue.add(async () => {
|
||||
return await database.execute(query);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Combined Resilience Service
|
||||
|
||||
```typescript
|
||||
// src/core/resilience/resilience.service.ts
|
||||
import CircuitBreaker from 'opossum';
|
||||
import { logger } from '@goodgo/logger';
|
||||
|
||||
interface ResilienceOptions {
|
||||
timeout?: number;
|
||||
maxRetries?: number;
|
||||
circuitBreaker?: boolean;
|
||||
fallback?: () => Promise<any>;
|
||||
}
|
||||
|
||||
export class ResilienceService {
|
||||
async execute<T>(
|
||||
operation: () => Promise<T>,
|
||||
name: string,
|
||||
options: ResilienceOptions = {}
|
||||
): Promise<T> {
|
||||
const {
|
||||
timeout = 5000,
|
||||
maxRetries = 3,
|
||||
circuitBreaker = true,
|
||||
fallback
|
||||
} = options;
|
||||
|
||||
let fn = operation;
|
||||
|
||||
// Wrap with timeout
|
||||
fn = () => this.withTimeout(operation(), timeout);
|
||||
|
||||
// Wrap with retry
|
||||
fn = () => this.retryWithBackoff(fn, maxRetries);
|
||||
|
||||
// Wrap with circuit breaker
|
||||
if (circuitBreaker) {
|
||||
const breaker = this.createCircuitBreaker(fn, name);
|
||||
try {
|
||||
return await breaker.fire();
|
||||
} catch (error) {
|
||||
if (fallback) {
|
||||
logger.warn(`${name}: Using fallback`, { error: error.message });
|
||||
return await fallback();
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return await fn();
|
||||
} catch (error) {
|
||||
if (fallback) {
|
||||
return await fallback();
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
|
||||
const timeout = new Promise<never>((_, reject) => {
|
||||
setTimeout(() => reject(new Error('Operation timeout')), ms);
|
||||
});
|
||||
return Promise.race([promise, timeout]);
|
||||
}
|
||||
|
||||
private async retryWithBackoff<T>(
|
||||
fn: () => Promise<T>,
|
||||
maxRetries: number
|
||||
): Promise<T> {
|
||||
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (error) {
|
||||
if (attempt === maxRetries) throw error;
|
||||
const delay = 1000 * Math.pow(2, attempt);
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
}
|
||||
}
|
||||
throw new Error('Retry exhausted');
|
||||
}
|
||||
|
||||
private createCircuitBreaker<T>(
|
||||
fn: () => Promise<T>,
|
||||
name: string
|
||||
): CircuitBreaker<[], T> {
|
||||
return new CircuitBreaker(fn, {
|
||||
timeout: 3000,
|
||||
errorThresholdPercentage: 50,
|
||||
resetTimeout: 30000,
|
||||
name
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
const resilience = new ResilienceService();
|
||||
|
||||
const result = await resilience.execute(
|
||||
() => externalApi.fetchUser(userId),
|
||||
'fetch-user',
|
||||
{
|
||||
timeout: 3000,
|
||||
maxRetries: 2,
|
||||
fallback: () => Promise.resolve({ id: userId, name: 'Unknown' })
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
### Health Check with Resilience
|
||||
|
||||
```typescript
|
||||
// src/health/health.controller.ts
|
||||
export class HealthController {
|
||||
async checkDependencies(): Promise<HealthStatus> {
|
||||
const checks = await Promise.allSettled([
|
||||
this.checkDatabase(),
|
||||
this.checkRedis(),
|
||||
this.checkExternalApi()
|
||||
]);
|
||||
|
||||
const results = {
|
||||
database: checks[0].status === 'fulfilled' ? 'healthy' : 'unhealthy',
|
||||
redis: checks[1].status === 'fulfilled' ? 'healthy' : 'unhealthy',
|
||||
externalApi: checks[2].status === 'fulfilled' ? 'healthy' : 'degraded'
|
||||
};
|
||||
|
||||
// Service is healthy even if external API is down (graceful degradation)
|
||||
const isHealthy = results.database === 'healthy' && results.redis === 'healthy';
|
||||
|
||||
return {
|
||||
status: isHealthy ? 'healthy' : 'unhealthy',
|
||||
dependencies: results
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Circuit Breaker**: Use for external service calls
|
||||
2. **Retry**: Retry only transient failures (network, timeout)
|
||||
3. **Timeout**: Set appropriate timeouts for all external calls
|
||||
4. **Fallback**: Always provide fallback behavior
|
||||
5. **Monitoring**: Monitor circuit breaker states and retry rates
|
||||
6. **Logging**: Log all resilience actions for debugging
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
1. **Retrying Non-Retryable Errors**: Retrying 4xx errors (client errors)
|
||||
```typescript
|
||||
// ❌ BAD: Retry all errors
|
||||
catch (error) {
|
||||
await retry(operation);
|
||||
}
|
||||
|
||||
// ✅ GOOD: Only retry transient errors
|
||||
catch (error) {
|
||||
if (isTransientError(error)) {
|
||||
await retry(operation);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. **No Timeout**: Missing timeouts on external calls
|
||||
```typescript
|
||||
// ❌ BAD: No timeout
|
||||
const data = await externalApi.fetch();
|
||||
|
||||
// ✅ GOOD: With timeout
|
||||
const data = await withTimeout(externalApi.fetch(), 5000);
|
||||
```
|
||||
|
||||
3. **No Fallback**: No graceful degradation strategy
|
||||
```typescript
|
||||
// ❌ BAD: Service crashes if dependency fails
|
||||
const user = await userService.get(id);
|
||||
|
||||
// ✅ GOOD: Fallback to cached/default data
|
||||
const user = await userService.get(id).catch(() => cachedUser);
|
||||
```
|
||||
|
||||
4. **Too Many Retries**: Excessive retries causing performance issues
|
||||
```typescript
|
||||
// ❌ BAD: Too many retries with short delay
|
||||
retry(fn, { maxRetries: 10, delay: 100 });
|
||||
|
||||
// ✅ GOOD: Limited retries with exponential backoff
|
||||
retry(fn, { maxRetries: 3, baseDelay: 1000, exponential: true });
|
||||
```
|
||||
|
||||
5. **Circuit Breaker Misconfiguration**: Wrong thresholds
|
||||
```typescript
|
||||
// ❌ BAD: Circuit opens too easily or never
|
||||
{ errorThresholdPercentage: 5 } // Opens after 5% errors
|
||||
{ errorThresholdPercentage: 99 } // Almost never opens
|
||||
|
||||
// ✅ GOOD: Balanced threshold
|
||||
{ errorThresholdPercentage: 50, resetTimeout: 30000 }
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Pattern | Use Case | Key Config |
|
||||
|---------|----------|------------|
|
||||
| **Circuit Breaker** | External API calls | threshold: 50%, reset: 30s |
|
||||
| **Retry** | Transient failures | max: 3, exponential backoff |
|
||||
| **Timeout** | All external calls | 3-5s for API, 30s for batch |
|
||||
| **Bulkhead** | Resource isolation | 10-20 concurrent ops |
|
||||
| **Fallback** | Critical operations | Cache, default, or degraded |
|
||||
|
||||
**Opossum Circuit Breaker States:**
|
||||
```
|
||||
CLOSED → (errors exceed threshold) → OPEN
|
||||
OPEN → (reset timeout expires) → HALF-OPEN
|
||||
HALF-OPEN → (success) → CLOSED
|
||||
HALF-OPEN → (failure) → OPEN
|
||||
```
|
||||
|
||||
**Retry Delays (Exponential Backoff):**
|
||||
```
|
||||
Attempt 1: 1s
|
||||
Attempt 2: 2s
|
||||
Attempt 3: 4s
|
||||
Attempt 4: 8s
|
||||
```
|
||||
|
||||
**Essential Imports:**
|
||||
```typescript
|
||||
import CircuitBreaker from 'opossum';
|
||||
import PQueue from 'p-queue';
|
||||
import { logger } from '@goodgo/logger';
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
- [Opossum Documentation](https://nodeshift.dev/opossum/) - Circuit breaker library
|
||||
- [Microsoft Resilience Patterns](https://docs.microsoft.com/en-us/azure/architecture/patterns/category/resiliency)
|
||||
- [API Gateway Advanced](../api-gateway-advanced/SKILL.md) - Gateway circuit breaker
|
||||
- [Observability & Monitoring](../observability-monitoring/SKILL.md) - Health checks, metrics
|
||||
- [Event-Driven Architecture](../event-driven-architecture/SKILL.md) - Event retry patterns
|
||||
- [Error Handling Patterns](../error-handling-patterns/SKILL.md) - Error handling
|
||||
- [Project Rules](../project-rules/SKILL.md) - GoodGo standards
|
||||
@@ -1,305 +0,0 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
|
||||
# Security Patterns for GoodGo Microservices
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use this skill when:
|
||||
- Implementing authentication and authorization in any service
|
||||
- Protecting sensitive data (PII, credentials, tokens)
|
||||
- Validating user inputs and file uploads
|
||||
- Implementing rate limiting and DDoS protection
|
||||
- Setting up audit logging and security monitoring
|
||||
- Encrypting data at rest and in transit
|
||||
- Managing secrets and credentials
|
||||
- Implementing security testing
|
||||
- Handling security incidents
|
||||
- Designing secure API endpoints
|
||||
|
||||
## Core Security Principles
|
||||
|
||||
1. **Defense in Depth**: Multiple layers of security controls
|
||||
2. **Least Privilege**: Grant minimum required permissions
|
||||
3. **Fail Secure**: Default to deny access
|
||||
4. **Separation of Duties**: Critical operations require multiple approvals
|
||||
5. **Audit Everything**: Log all security-relevant events
|
||||
6. **Encrypt Sensitive Data**: PII, tokens, credentials must be encrypted
|
||||
7. **Validate All Inputs**: Never trust user input
|
||||
8. **Principle of Least Exposure**: Minimize attack surface
|
||||
9. **Secure by Default**: Security built-in, not bolted on
|
||||
10. **Assume Breach**: Design for detection and response
|
||||
|
||||
## Authentication & Authorization
|
||||
|
||||
### JWT Token Validation
|
||||
|
||||
Extract tokens from Authorization header or cookies, verify with `jwtService.verifyAccessToken()`, and attach user payload to request. Return 401 for missing/invalid tokens.
|
||||
|
||||
```typescript
|
||||
// Key pattern - see references/REFERENCE.md for full implementation
|
||||
const authHeader = req.headers.authorization;
|
||||
const token = authHeader?.startsWith('Bearer ') ? authHeader.substring(7) : req.cookies?.access_token;
|
||||
const payload = await jwtService.verifyAccessToken(token);
|
||||
req.user = { id: payload.sub, roles: payload.roles || [], permissions: payload.permissions || [] };
|
||||
```
|
||||
|
||||
### Role-Based Authorization (RBAC)
|
||||
|
||||
Use `requireRole()` middleware for role checks and `requirePermission()` for fine-grained access control with `resource:action` format.
|
||||
|
||||
```typescript
|
||||
// Usage pattern
|
||||
router.post('/api/v1/users', authenticate(), requirePermission('users', 'create'), userController.create);
|
||||
router.delete('/api/v1/admin', authenticate(), requireRole('admin', 'superadmin'), adminController.delete);
|
||||
```
|
||||
|
||||
### Resource Ownership
|
||||
|
||||
Validate users can only access their own resources using `requireOwnership()` middleware.
|
||||
|
||||
## Data Protection
|
||||
|
||||
### Encryption
|
||||
|
||||
- Use AES-256-GCM for encrypting PII at rest
|
||||
- Store encrypted data as `iv:tag:ciphertext` format
|
||||
- Require 32+ character ENCRYPTION_KEY
|
||||
|
||||
### Password Hashing
|
||||
|
||||
- Always use bcrypt with cost factor 12 in production
|
||||
- Never log passwords - sanitize before logging
|
||||
|
||||
### Token Storage
|
||||
|
||||
- Hash tokens with SHA-256 before database storage
|
||||
- Use `crypto.randomBytes(32)` for secure token generation
|
||||
|
||||
## Input Validation
|
||||
|
||||
### Zod Schema Validation
|
||||
|
||||
Always validate inputs with Zod schemas before processing:
|
||||
|
||||
```typescript
|
||||
const CreateUserDto = z.object({
|
||||
email: z.string().email(),
|
||||
password: z.string().min(8).regex(/[A-Z]/).regex(/[a-z]/).regex(/[0-9]/).regex(/[^A-Za-z0-9]/),
|
||||
phone: z.string().regex(/^\+[1-9]\d{1,14}$/).optional(),
|
||||
name: z.string().min(1).max(255)
|
||||
});
|
||||
|
||||
// In controller
|
||||
const dto = CreateUserDto.parse(req.body);
|
||||
```
|
||||
|
||||
### File Upload Validation
|
||||
|
||||
- Check file size (max 10MB default)
|
||||
- Validate MIME type against whitelist
|
||||
- Verify content with `file-type` library to prevent MIME spoofing
|
||||
|
||||
### SQL Injection Prevention
|
||||
|
||||
Always use Prisma parameterized queries - never string concatenation for queries.
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
Configure Redis-backed rate limiting for all endpoints:
|
||||
|
||||
| Limiter Type | Window | Max Requests | Use Case |
|
||||
|-------------|--------|--------------|----------|
|
||||
| Standard | 15 min | 100 | General API endpoints |
|
||||
| Strict | 1 hour | 10 | Sensitive operations |
|
||||
| Login | 15 min | 5 | Authentication endpoints |
|
||||
|
||||
```typescript
|
||||
router.post('/api/v1/auth/login', loginLimiter, authController.login);
|
||||
```
|
||||
|
||||
## Security Headers
|
||||
|
||||
Use Helmet middleware with CSP, HSTS, and additional headers:
|
||||
- `X-Content-Type-Options: nosniff`
|
||||
- `X-Frame-Options: DENY`
|
||||
- `X-XSS-Protection: 1; mode=block`
|
||||
- `Referrer-Policy: strict-origin-when-cross-origin`
|
||||
|
||||
## CORS Configuration
|
||||
|
||||
- Whitelist allowed origins from environment variables
|
||||
- Enable credentials for authenticated requests
|
||||
- Set `maxAge: 86400` (24 hours) for preflight caching
|
||||
|
||||
## Secrets Management
|
||||
|
||||
- Never hardcode secrets - use environment variables
|
||||
- Validate secrets with Zod schema at startup
|
||||
- Use secret managers in production (AWS Secrets Manager, Vault, etc.)
|
||||
- Rotate secrets quarterly
|
||||
|
||||
```typescript
|
||||
const secretsSchema = z.object({
|
||||
JWT_SECRET: z.string().min(32),
|
||||
DATABASE_URL: z.string().url(),
|
||||
ENCRYPTION_KEY: z.string().min(32).optional()
|
||||
});
|
||||
```
|
||||
|
||||
## Audit Logging
|
||||
|
||||
Log all security-relevant events with sanitized details:
|
||||
|
||||
```typescript
|
||||
await auditService.logSecurityEvent('LOGIN_SUCCESS', user.id, { email: user.email }, req);
|
||||
await auditService.logSecurityEvent('PERMISSION_DENIED', user.id, { resource, action }, req);
|
||||
```
|
||||
|
||||
Sanitize sensitive fields: password, token, secret, ssn, creditCard.
|
||||
|
||||
## Error Handling
|
||||
|
||||
- Log full errors internally
|
||||
- Never expose user existence (use generic "Invalid credentials")
|
||||
- Show stack traces only in development
|
||||
- Return sanitized error codes in production
|
||||
|
||||
## Security Testing
|
||||
|
||||
Write tests for:
|
||||
- SQL injection prevention
|
||||
- XSS attack prevention
|
||||
- Authentication enforcement
|
||||
- Authorization enforcement
|
||||
- Rate limiting effectiveness
|
||||
|
||||
## Best Practices
|
||||
|
||||
- All endpoints require authentication (except public)
|
||||
- Authorization checks at every protected route
|
||||
- Input validation with Zod on all user input
|
||||
- Rate limiting on all endpoints
|
||||
- Error messages sanitized in production
|
||||
- PII encrypted at rest with AES-256-GCM
|
||||
- Passwords hashed with bcrypt (cost 12+)
|
||||
- Tokens hashed before database storage
|
||||
- HTTPS enforced (TLS 1.2+)
|
||||
- Security headers via Helmet
|
||||
- Audit logging for security events
|
||||
- Dependencies scanned for vulnerabilities
|
||||
- File uploads validated (size, type, content)
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
### 1. Weak Password Hashing
|
||||
|
||||
```typescript
|
||||
// BAD: Low cost factor
|
||||
const hash = await bcrypt.hash(password, 8);
|
||||
|
||||
// GOOD: Use cost 12
|
||||
const hash = await bcrypt.hash(password, 12);
|
||||
```
|
||||
|
||||
### 2. Hardcoded Secrets
|
||||
|
||||
```typescript
|
||||
// BAD
|
||||
const JWT_SECRET = "my-secret-key";
|
||||
|
||||
// GOOD
|
||||
const JWT_SECRET = process.env.JWT_SECRET;
|
||||
if (!JWT_SECRET) throw new Error('JWT_SECRET required');
|
||||
```
|
||||
|
||||
### 3. Missing Input Validation
|
||||
|
||||
```typescript
|
||||
// BAD
|
||||
const user = await prisma.user.findUnique({ where: { id: req.params.id } });
|
||||
|
||||
// GOOD
|
||||
const { id } = z.object({ id: z.string().cuid() }).parse(req.params);
|
||||
const user = await prisma.user.findUnique({ where: { id } });
|
||||
```
|
||||
|
||||
### 4. Logging Sensitive Data
|
||||
|
||||
```typescript
|
||||
// BAD
|
||||
logger.info('User login', { email, password });
|
||||
|
||||
// GOOD
|
||||
logger.info('User login', { email, password: '[REDACTED]' });
|
||||
```
|
||||
|
||||
### 5. Exposing User Existence
|
||||
|
||||
```typescript
|
||||
// BAD
|
||||
if (!user) throw new Error('User not found');
|
||||
|
||||
// GOOD
|
||||
if (!user || !await bcrypt.compare(password, user.passwordHash)) {
|
||||
throw new Error('Invalid credentials');
|
||||
}
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Security Area | Implementation |
|
||||
|--------------|----------------|
|
||||
| **Password hashing** | `bcrypt.hash(password, 12)` |
|
||||
| **JWT Access Token** | 15 minutes expiry |
|
||||
| **JWT Refresh Token** | 7 days expiry |
|
||||
| **Rate limiting** | Standard: 100/15min, Strict: 10/hour, Login: 5/15min |
|
||||
| **Encryption** | AES-256-GCM for PII |
|
||||
| **Input validation** | Zod schemas, always parse before use |
|
||||
| **SQL injection** | Use Prisma (parameterized by default) |
|
||||
| **Security headers** | helmet middleware |
|
||||
| **CORS** | Whitelist origins, credentials: true |
|
||||
|
||||
**Essential Imports:**
|
||||
|
||||
```typescript
|
||||
import bcrypt from 'bcrypt';
|
||||
import helmet from 'helmet';
|
||||
import rateLimit from 'express-rate-limit';
|
||||
import { z } from 'zod';
|
||||
import { jwtService } from '@goodgo/auth-sdk';
|
||||
```
|
||||
|
||||
## Security Checklist
|
||||
|
||||
Before deploying any service:
|
||||
|
||||
- [ ] All endpoints require authentication (except public)
|
||||
- [ ] Authorization checks implemented (RBAC/ABAC)
|
||||
- [ ] Input validation with Zod schemas
|
||||
- [ ] Rate limiting configured
|
||||
- [ ] Error messages sanitized (no info disclosure)
|
||||
- [ ] PII encrypted at rest
|
||||
- [ ] Passwords hashed with bcrypt (cost 12+)
|
||||
- [ ] Tokens hashed before storing
|
||||
- [ ] Secrets in environment variables (never hardcoded)
|
||||
- [ ] HTTPS enforced (TLS 1.2+)
|
||||
- [ ] CORS configured correctly
|
||||
- [ ] Security headers set (helmet)
|
||||
- [ ] Audit logging enabled
|
||||
- [ ] SQL injection prevented (use Prisma)
|
||||
- [ ] XSS prevention (input sanitization)
|
||||
- [ ] File upload validation
|
||||
- [ ] Security tests passing
|
||||
- [ ] Dependencies scanned for vulnerabilities
|
||||
- [ ] Secrets rotation plan in place
|
||||
|
||||
## Resources
|
||||
|
||||
- [Detailed Code Examples](./references/REFERENCE.md) - Full implementation examples
|
||||
- [OWASP Top 10](https://owasp.org/www-project-top-ten/)
|
||||
- [OWASP API Security Top 10](https://owasp.org/www-project-api-security/)
|
||||
- [Node.js Security Best Practices](https://nodejs.org/en/docs/guides/security/)
|
||||
- [Express Security Best Practices](https://expressjs.com/en/advanced/best-practice-security.html)
|
||||
@@ -1,20 +0,0 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
# Service Discovery & Registry Patterns
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use this skill when:
|
||||
- Implementing authentication and authorization in any service
|
||||
- Protecting sensitive data (PII, credentials, tokens)
|
||||
- Validating user inputs and file uploads
|
||||
- Implementing rate limiting and DDoS protection
|
||||
- Setting up audit logging and security monitoring
|
||||
- Encrypting data at rest and in transit
|
||||
- Managing secrets and credentials
|
||||
- Implementing security testing
|
||||
- Handling security incidents
|
||||
- Designing secure API endpoints
|
||||
|
||||
@@ -1,248 +0,0 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
# Service Layer Patterns
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use this skill when:
|
||||
- Implementing business logic in services
|
||||
- Organizing service layer code
|
||||
- Using dependency injection patterns
|
||||
- Composing multiple services together
|
||||
- Separating concerns between controllers, services, and repositories
|
||||
- Handling business rules and validations
|
||||
- Implementing service composition patterns
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Service Layer Responsibilities
|
||||
|
||||
The service layer:
|
||||
- Contains business logic
|
||||
- Orchestrates repository calls
|
||||
- Validates business rules
|
||||
- Handles cross-cutting concerns (caching, logging)
|
||||
- Coordinates multiple repositories
|
||||
- Independent of HTTP transport layer
|
||||
|
||||
### Dependency Injection
|
||||
|
||||
Services use constructor injection for dependencies:
|
||||
|
||||
```typescript
|
||||
export class UserService {
|
||||
constructor(
|
||||
private userRepository: UserRepository,
|
||||
private cacheService: CacheService
|
||||
) {}
|
||||
}
|
||||
```
|
||||
|
||||
## Patterns
|
||||
|
||||
### Basic Service Pattern
|
||||
|
||||
```typescript
|
||||
import { logger } from '@goodgo/logger';
|
||||
import { userRepository } from '../repositories/user.repository';
|
||||
import { NotFoundError } from '../errors/http-error';
|
||||
|
||||
export class UserService {
|
||||
async getUserById(id: string) {
|
||||
logger.info('Fetching user by ID', { id });
|
||||
|
||||
const user = await userRepository.findById(id);
|
||||
if (!user) {
|
||||
throw new NotFoundError('User', { id });
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Service with Caching
|
||||
|
||||
```typescript
|
||||
export class UserService {
|
||||
constructor(
|
||||
private repository: UserRepository,
|
||||
private cacheService: CacheService
|
||||
) {}
|
||||
|
||||
async getUserById(id: string) {
|
||||
const cacheKey = cacheService.keys.user(id);
|
||||
|
||||
// Try cache first
|
||||
const cached = await this.cacheService.get<User>(cacheKey);
|
||||
if (cached) return cached;
|
||||
|
||||
// Cache miss - fetch from repository
|
||||
const user = await this.repository.findById(id);
|
||||
if (!user) {
|
||||
throw new NotFoundError('User');
|
||||
}
|
||||
|
||||
// Cache for 5 minutes
|
||||
await this.cacheService.set(cacheKey, user, 300);
|
||||
return user;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Service Composition
|
||||
|
||||
Services can depend on other services:
|
||||
|
||||
```typescript
|
||||
export class AccessRequestService {
|
||||
constructor(
|
||||
private accessRequestRepository: AccessRequestRepository,
|
||||
private userService: UserService,
|
||||
private rbacService: RBACService
|
||||
) {}
|
||||
|
||||
async createRequest(userId: string, data: CreateRequestDto) {
|
||||
// Use other services
|
||||
const user = await this.userService.getUserById(userId);
|
||||
const hasPermission = await this.rbacService.checkPermission(userId, 'CREATE_REQUEST');
|
||||
|
||||
if (!hasPermission) {
|
||||
throw new ForbiddenError('Insufficient permissions');
|
||||
}
|
||||
|
||||
return await this.accessRequestRepository.create({ ...data, userId });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Business Logic Validation
|
||||
|
||||
Services validate business rules:
|
||||
|
||||
```typescript
|
||||
export class UserService {
|
||||
async createUser(data: CreateUserInput) {
|
||||
// Business rule: Check if email exists
|
||||
const existing = await this.repository.findByEmail(data.email);
|
||||
if (existing) {
|
||||
throw new ConflictError('User with this email already exists');
|
||||
}
|
||||
|
||||
// Business rule: Validate organization
|
||||
if (data.organizationId) {
|
||||
const org = await this.orgRepository.findById(data.organizationId);
|
||||
if (!org) {
|
||||
throw new NotFoundError('Organization');
|
||||
}
|
||||
}
|
||||
|
||||
return await this.repository.create(data);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Service Module Pattern
|
||||
|
||||
Organize services into modules:
|
||||
|
||||
```typescript
|
||||
export class FeatureModule {
|
||||
private controller: FeatureController;
|
||||
private service: FeatureService;
|
||||
private router: Router;
|
||||
|
||||
constructor() {
|
||||
const repository = new FeatureRepository(prisma);
|
||||
this.service = new FeatureService(repository);
|
||||
this.controller = new FeatureController(this.service);
|
||||
this.router = this.createRouter();
|
||||
}
|
||||
|
||||
getRouter(): Router {
|
||||
return this.router;
|
||||
}
|
||||
|
||||
private createRouter(): Router {
|
||||
const router = Router();
|
||||
router.get('/', asyncHandler(this.controller.findAll.bind(this.controller)));
|
||||
router.post('/', asyncHandler(this.controller.create.bind(this.controller)));
|
||||
return router;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Single Responsibility**: Each service handles one domain area
|
||||
2. **Dependency Injection**: Use constructor injection for testability
|
||||
3. **Business Logic Only**: Keep HTTP concerns in controllers
|
||||
4. **Use Repositories**: Don't access database directly
|
||||
5. **Error Handling**: Throw appropriate domain errors
|
||||
6. **Logging**: Log important operations and errors
|
||||
7. **Caching**: Implement caching in services, not repositories
|
||||
8. **Composition**: Compose services for complex operations
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
1. **HTTP in Services**: Using `req`/`res` in services
|
||||
2. **Direct Database Access**: Accessing Prisma directly instead of repositories
|
||||
3. **Too Many Responsibilities**: Service doing too many things
|
||||
4. **No Error Handling**: Not throwing appropriate errors
|
||||
5. **Business Logic in Controllers**: Moving business logic to controllers
|
||||
|
||||
## Quick Reference
|
||||
|
||||
**Service Responsibilities:**
|
||||
| Layer | Responsibility | Example |
|
||||
|-------|----------------|---------|
|
||||
| **Controller** | HTTP handling, validation | Parse request, send response |
|
||||
| **Service** | Business logic | Validate rules, orchestrate |
|
||||
| **Repository** | Data access | CRUD operations |
|
||||
|
||||
**Service Template:**
|
||||
```typescript
|
||||
export class EntityService {
|
||||
constructor(
|
||||
private repository: EntityRepository,
|
||||
private cache?: CacheService
|
||||
) {}
|
||||
|
||||
async findById(id: string): Promise<Entity> {
|
||||
const entity = await this.repository.findById(id);
|
||||
if (!entity) throw new NotFoundError('Entity');
|
||||
return entity;
|
||||
}
|
||||
|
||||
async create(data: CreateDto): Promise<Entity> {
|
||||
// Business validation
|
||||
await this.validateBusinessRules(data);
|
||||
return this.repository.create(data);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Dependency Injection Pattern:**
|
||||
```typescript
|
||||
// Module setup
|
||||
const repository = new EntityRepository(prisma);
|
||||
const service = new EntityService(repository, cacheService);
|
||||
const controller = new EntityController(service);
|
||||
```
|
||||
|
||||
**Common Patterns:**
|
||||
| Pattern | When to Use |
|
||||
|---------|-------------|
|
||||
| **Caching** | Frequently accessed data |
|
||||
| **Composition** | Complex operations across domains |
|
||||
| **Validation** | Business rule enforcement |
|
||||
| **Logging** | Audit and debugging |
|
||||
|
||||
## Resources
|
||||
|
||||
- [Feature Service](../../services/iam-service/src/modules/feature/feature.service.ts) - Example service implementation
|
||||
- [Repository Pattern](../repository-pattern/SKILL.md) - Repository patterns
|
||||
- [Caching Patterns](../caching-patterns/SKILL.md) - Caching in services
|
||||
- [Error Handling](../error-handling-patterns/SKILL.md) - Error handling patterns
|
||||
@@ -1,179 +0,0 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
# Testing Patterns for GoodGo Microservices
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use this skill when:
|
||||
- Writing unit tests for services, controllers, or repositories
|
||||
- Creating integration tests for middleware chains
|
||||
- Building E2E tests for API endpoints
|
||||
- Setting up Jest configuration for a new service
|
||||
- Mocking external dependencies (Prisma, Redis, Auth SDK)
|
||||
- Debugging test failures
|
||||
- Improving test coverage
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Test Types
|
||||
|
||||
| Type | Location | Speed | Dependencies |
|
||||
|------|----------|-------|--------------|
|
||||
| **Unit** | `*.test.ts` (next to source) | <1s | All mocked |
|
||||
| **Integration** | `__tests__/` | 1-5s | Partial mocking |
|
||||
| **E2E** | `__tests__/*.e2e.ts` | 5-10s | Test DB, mocked external |
|
||||
|
||||
## Key Patterns
|
||||
|
||||
### Jest Configuration
|
||||
|
||||
```typescript
|
||||
// jest.config.ts
|
||||
export default {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
testMatch: ['**/__tests__/**/*.test.ts', '**/__tests__/**/*.e2e.ts'],
|
||||
coverageThreshold: { global: { branches: 70, functions: 70, lines: 70 } },
|
||||
setupFilesAfterEnv: ['<rootDir>/src/__tests__/setupTests.ts'],
|
||||
};
|
||||
```
|
||||
|
||||
### Unit Test Pattern
|
||||
|
||||
```typescript
|
||||
describe('FeatureService', () => {
|
||||
let service: FeatureService;
|
||||
let mockRepository: any;
|
||||
|
||||
beforeEach(() => {
|
||||
mockRepository = { findById: jest.fn(), create: jest.fn() };
|
||||
service = new FeatureService(mockRepository);
|
||||
});
|
||||
|
||||
it('should return feature when found', async () => {
|
||||
mockRepository.findById.mockResolvedValue({ id: '1', name: 'Test' });
|
||||
const result = await service.findById('1');
|
||||
expect(result).toEqual({ id: '1', name: 'Test' });
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### E2E Test Pattern
|
||||
|
||||
```typescript
|
||||
describe('POST /api/features', () => {
|
||||
it('should create a new feature', async () => {
|
||||
const response = await request
|
||||
.post('/api/features')
|
||||
.set('Authorization', 'Bearer valid-token')
|
||||
.send({ name: 'New Feature' })
|
||||
.expect(201);
|
||||
|
||||
expect(response.body).toMatchObject({ success: true, data: { name: 'New Feature' } });
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Mock Prisma
|
||||
|
||||
```typescript
|
||||
import { mockDeep } from 'jest-mock-extended';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
export const prismaMock = mockDeep<PrismaClient>();
|
||||
jest.mock('../prisma', () => ({ default: prismaMock }));
|
||||
|
||||
// Usage
|
||||
prismaMock.user.create.mockResolvedValue({ id: '1', email: 'test@example.com' });
|
||||
```
|
||||
|
||||
### Test Factory
|
||||
|
||||
```typescript
|
||||
export class TestFactory {
|
||||
static createUser(overrides = {}) {
|
||||
return { id: 'test-user-1', email: 'test@example.com', ...overrides };
|
||||
}
|
||||
static createAuthToken(userId: string) {
|
||||
return jwt.sign({ userId }, 'test-secret');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
- Each test is independent and isolated
|
||||
- Tests follow AAA pattern (Arrange-Act-Assert)
|
||||
- Mock external dependencies
|
||||
- Test edge cases and error scenarios
|
||||
- Keep tests simple and focused
|
||||
- Use descriptive test names
|
||||
- Maintain >70% code coverage
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
1. **Testing Implementation Details**: Tests break on refactoring
|
||||
```typescript
|
||||
// BAD: expect(service['privateMethod']()).toBe(true);
|
||||
// GOOD: expect(await service.processOrder(order)).toEqual({ success: true });
|
||||
```
|
||||
|
||||
2. **Shared Mutable State**: Tests affect each other
|
||||
```typescript
|
||||
// BAD: let counter = 0; (shared across tests)
|
||||
// GOOD: beforeEach(() => { counter = 0; });
|
||||
```
|
||||
|
||||
3. **Not Mocking External Services**: Tests are slow and flaky
|
||||
```typescript
|
||||
// BAD: await fetch('https://api.example.com/data');
|
||||
// GOOD: jest.spyOn(httpClient, 'get').mockResolvedValue({ data: mockData });
|
||||
```
|
||||
|
||||
4. **Missing Edge Cases**: Only testing happy path
|
||||
```typescript
|
||||
// GOOD: Include error cases
|
||||
test('creates user', async () => { ... });
|
||||
test('throws on duplicate email', async () => { ... });
|
||||
test('validates email format', async () => { ... });
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
**Coverage Targets:**
|
||||
- Global: 70%+ (branches, functions, lines)
|
||||
- Critical paths: 90%+
|
||||
- Repositories/Services: 80%+
|
||||
|
||||
**Essential Commands:**
|
||||
```bash
|
||||
pnpm test # Run all tests
|
||||
pnpm test:watch # Watch mode
|
||||
pnpm test:coverage # With coverage
|
||||
pnpm test -- --runInBand # Sequential (for debugging)
|
||||
pnpm test -- UserService # Run specific test file
|
||||
```
|
||||
|
||||
**Mock Imports:**
|
||||
```typescript
|
||||
import { mockDeep } from 'jest-mock-extended';
|
||||
import { prismaMock } from '../__mocks__/prisma';
|
||||
import supertest from 'supertest';
|
||||
```
|
||||
|
||||
**Debug Tips:**
|
||||
1. Use `test.only()` to run single test
|
||||
2. Use `--detectOpenHandles` for async issues
|
||||
3. Use `--runInBand` for sequential execution
|
||||
4. Add `console.log()` statements temporarily
|
||||
|
||||
## Resources
|
||||
|
||||
- [Jest Documentation](https://jestjs.io/docs/getting-started) - Testing framework
|
||||
- [Supertest](https://github.com/ladjs/supertest) - HTTP assertions
|
||||
- [jest-mock-extended](https://github.com/marchaos/jest-mock-extended) - TypeScript mocks
|
||||
- [Detailed Code Examples](./references/REFERENCE.md)
|
||||
- [Database Prisma](../database-prisma/SKILL.md) - Database mocking patterns
|
||||
- [Error Handling](../error-handling-patterns/SKILL.md) - Error testing
|
||||
Reference in New Issue
Block a user