Update project configuration and enhance service template with new features
- Updated `.gitignore` to clarify environment variable handling. - Enhanced `pnpm-lock.yaml` with new dependencies for Jest and Supertest, including type definitions. - Improved bilingual documentation in `SKILL.md` files for better clarity on comment patterns and project rules. - Refined `docker-compose.yml` for local development, adding detailed instructions and access points. - Updated environment variable example in `env.local.example` for better guidance on configuration. - Removed outdated architecture documentation from the service template. - Enhanced Dockerfile for improved security and performance during builds. - Added Swagger documentation setup in the service template for better API documentation. - Improved error handling and logging middleware for enhanced debugging capabilities.
This commit is contained in:
450
.cursor/plans/create_cursor_skills_14de746a.plan.md
Normal file
450
.cursor/plans/create_cursor_skills_14de746a.plan.md
Normal file
@@ -0,0 +1,450 @@
|
||||
---
|
||||
name: Create Cursor Skills
|
||||
overview: Create 5 comprehensive Cursor Skills (400-500 lines each, English only) covering testing, API design, database, observability, and Kubernetes deployment patterns based on existing codebase patterns.
|
||||
todos:
|
||||
- id: create-testing-skill
|
||||
content: Create testing-patterns skill with Jest config, mocking strategies, and test examples
|
||||
status: completed
|
||||
- id: create-api-skill
|
||||
content: Create api-design skill with RESTful patterns, DTO validation, and OpenAPI docs
|
||||
status: completed
|
||||
- id: create-database-skill
|
||||
content: Create database-prisma skill with repository pattern, migrations, and query optimization
|
||||
status: completed
|
||||
- id: create-observability-skill
|
||||
content: Create observability-monitoring skill with metrics, logging, tracing, and health checks
|
||||
status: completed
|
||||
- id: create-kubernetes-skill
|
||||
content: Create deployment-kubernetes skill with K8s manifests, HPA, and deployment strategies
|
||||
status: completed
|
||||
---
|
||||
|
||||
# Create Comprehensive Cursor Skills
|
||||
|
||||
## Overview
|
||||
|
||||
Create 5 detailed Cursor Skills (400-500 lines each, English only) to codify best practices and patterns from the GoodGo microservices platform. Each skill will be based on actual code patterns found in the codebase.
|
||||
|
||||
## Skills to Create
|
||||
|
||||
### 1. testing-patterns
|
||||
|
||||
**Location**: `.cursor/skills/testing-patterns/SKILL.md`
|
||||
|
||||
**Content Structure**:
|
||||
|
||||
- Jest configuration patterns (from [`services/_template/jest.config.ts`](services/_template/jest.config.ts))
|
||||
- Test setup utilities (from [`services/_template/src/__tests__/setupTests.ts`](services/_template/src/__tests__/setupTests.ts))
|
||||
- Unit testing patterns (from feature.service.test.ts, feature.repository.test.ts)
|
||||
- Integration testing patterns (from health.controller.test.ts)
|
||||
- E2E testing patterns (from feature.e2e.ts, health.e2e.ts)
|
||||
- Mocking strategies:
|
||||
- Prisma mocking
|
||||
- Redis mocking
|
||||
- Auth SDK mocking
|
||||
- Logger mocking
|
||||
- Test utilities (createMockReq, createMockRes, createMockNext)
|
||||
- Coverage requirements (>70%)
|
||||
- Common testing mistakes
|
||||
- Debugging test failures
|
||||
|
||||
**Key Patterns to Document**:
|
||||
|
||||
```typescript
|
||||
// Mock setup pattern
|
||||
jest.mock('@goodgo/logger');
|
||||
jest.mock('../feature.repository');
|
||||
|
||||
// Test structure pattern
|
||||
describe('FeatureService', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should create feature successfully', async () => {
|
||||
// Arrange
|
||||
const mockData = {...};
|
||||
(repository.create as jest.Mock).mockResolvedValue(mockData);
|
||||
|
||||
// Act
|
||||
const result = await service.create(input);
|
||||
|
||||
// Assert
|
||||
expect(repository.create).toHaveBeenCalledWith(input);
|
||||
expect(result).toEqual(mockData);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 2. api-design
|
||||
|
||||
**Location**: `.cursor/skills/api-design/SKILL.md`
|
||||
|
||||
**Content Structure**:
|
||||
|
||||
- RESTful conventions (resource naming, HTTP methods)
|
||||
- Response format standards (from existing API responses)
|
||||
- Error response structure (from [`services/_template/src/errors/`](services/_template/src/errors/))
|
||||
- DTO validation with Zod (from [`services/_template/src/modules/feature/feature.dto.ts`](services/_template/src/modules/feature/feature.dto.ts))
|
||||
- Controller patterns (from feature.controller.ts)
|
||||
- Route organization (from feature.module.ts)
|
||||
- OpenAPI/Swagger documentation (from [`services/_template/src/docs/swagger.ts`](services/_template/src/docs/swagger.ts))
|
||||
- Pagination patterns
|
||||
- Filtering and sorting
|
||||
- API versioning (/api/v1/)
|
||||
- Authentication headers
|
||||
- Correlation ID propagation
|
||||
|
||||
**Key Patterns to Document**:
|
||||
|
||||
```typescript
|
||||
// DTO with Zod
|
||||
export const createFeatureDtoSchema = z.object({
|
||||
name: z.string().min(1).max(100),
|
||||
title: z.string().max(200).optional(),
|
||||
});
|
||||
|
||||
// Controller pattern
|
||||
export class FeatureController {
|
||||
async create(req: Request, res: Response, next: NextFunction) {
|
||||
const dto = createFeatureDtoSchema.parse(req.body);
|
||||
const result = await this.service.create(dto);
|
||||
res.status(201).json({ success: true, data: result });
|
||||
}
|
||||
}
|
||||
|
||||
// Route with middleware
|
||||
router.post('/',
|
||||
authenticate(),
|
||||
authorize('admin'),
|
||||
validateDto(createFeatureDtoSchema),
|
||||
asyncHandler(controller.create)
|
||||
);
|
||||
```
|
||||
|
||||
### 3. database-prisma
|
||||
|
||||
**Location**: `.cursor/skills/database-prisma/SKILL.md`
|
||||
|
||||
**Content Structure**:
|
||||
|
||||
- Prisma schema conventions (from [`services/_template/prisma/schema.prisma`](services/_template/prisma/schema.prisma))
|
||||
- Migration workflow (dev vs production)
|
||||
- Seeding strategies (from [`services/_template/prisma/seed.ts`](services/_template/prisma/seed.ts))
|
||||
- Repository pattern (from [`services/_template/src/modules/common/repository.ts`](services/_template/src/modules/common/repository.ts))
|
||||
- BaseRepository implementation
|
||||
- Feature-specific repositories (from feature.repository.ts)
|
||||
- Connection pooling with Neon
|
||||
- Transaction handling
|
||||
- Query optimization
|
||||
- Error handling (DatabaseError)
|
||||
- Soft delete pattern
|
||||
- Prisma Client generation
|
||||
|
||||
**Key Patterns to Document**:
|
||||
|
||||
```typescript
|
||||
// BaseRepository pattern
|
||||
export class BaseRepository<T, CreateInput, UpdateInput> {
|
||||
constructor(
|
||||
protected prisma: PrismaClient,
|
||||
protected modelName: string,
|
||||
protected delegate: any
|
||||
) {}
|
||||
|
||||
async findById(id: string): Promise<T | null> {
|
||||
try {
|
||||
return await this.delegate.findUnique({ where: { id } });
|
||||
} catch (error: any) {
|
||||
throw new DatabaseError(`Failed to find ${this.modelName}`, { id });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Feature-specific repository
|
||||
export class FeatureRepository extends BaseRepository<Feature, CreateFeatureInput, UpdateFeatureInput> {
|
||||
async findByName(name: string): Promise<Feature | null> {
|
||||
return this.findByUnique({ name });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. observability-monitoring
|
||||
|
||||
**Location**: `.cursor/skills/observability-monitoring/SKILL.md`
|
||||
|
||||
**Content Structure**:
|
||||
|
||||
- Prometheus metrics patterns (from [`services/_template/src/middlewares/metrics.middleware.ts`](services/_template/src/middlewares/metrics.middleware.ts))
|
||||
- Metric types (Counter, Gauge, Histogram, Summary)
|
||||
- Custom metrics creation
|
||||
- Correlation ID middleware (from correlation.middleware.ts)
|
||||
- Structured logging patterns (from logger.middleware.ts)
|
||||
- Jaeger tracing integration
|
||||
- Health check patterns (liveness vs readiness)
|
||||
- Prometheus configuration (from [`infra/observability/prometheus/prometheus.yml`](infra/observability/prometheus/prometheus.yml))
|
||||
- Grafana dashboard setup
|
||||
- Loki log aggregation
|
||||
- Alert rules
|
||||
- Debugging with observability tools
|
||||
|
||||
**Key Patterns to Document**:
|
||||
|
||||
```typescript
|
||||
// Metrics middleware pattern
|
||||
const httpRequestDurationSeconds = new client.Histogram({
|
||||
name: 'http_request_duration_seconds',
|
||||
help: 'Duration of HTTP requests in seconds',
|
||||
labelNames: ['method', 'route', 'status_code', 'correlation_id'],
|
||||
buckets: [0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 2, 5, 10],
|
||||
});
|
||||
|
||||
// Correlation ID pattern
|
||||
export const correlationMiddleware = () => {
|
||||
return (req: Request, res: Response, next: NextFunction) => {
|
||||
const correlationId = req.headers['x-correlation-id'] || generateCorrelationId();
|
||||
req.correlationId = correlationId;
|
||||
res.setHeader('X-Correlation-ID', correlationId);
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
// Health check pattern
|
||||
export class HealthController {
|
||||
async liveness(req: Request, res: Response) {
|
||||
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
||||
}
|
||||
|
||||
async readiness(req: Request, res: Response) {
|
||||
const dbHealthy = await checkDatabaseConnection();
|
||||
const redisHealthy = await checkRedisConnection();
|
||||
res.json({ status: dbHealthy && redisHealthy ? 'ok' : 'degraded' });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. deployment-kubernetes
|
||||
|
||||
**Location**: `.cursor/skills/deployment-kubernetes/SKILL.md`
|
||||
|
||||
**Content Structure**:
|
||||
|
||||
- Kubernetes manifest structure (from [`deployments/production/kubernetes/`](deployments/production/kubernetes/))
|
||||
- Deployment configuration
|
||||
- Service types (ClusterIP, LoadBalancer)
|
||||
- ConfigMap and Secrets management (from configmap.yaml, secrets.yaml.example)
|
||||
- Ingress configuration (from ingress.yaml)
|
||||
- HorizontalPodAutoscaler setup
|
||||
- Resource limits and requests
|
||||
- Health probes (liveness, readiness, startup)
|
||||
- Rolling update strategy
|
||||
- Environment variable management
|
||||
- Multi-environment setup (staging vs production)
|
||||
- Namespace organization
|
||||
- Service discovery in K8s
|
||||
- Troubleshooting K8s deployments
|
||||
|
||||
**Key Patterns to Document**:
|
||||
|
||||
```yaml
|
||||
# Deployment with HPA
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: auth-service
|
||||
namespace: production
|
||||
spec:
|
||||
replicas: 3
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: auth-service
|
||||
image: goodgo/auth-service:latest
|
||||
resources:
|
||||
requests:
|
||||
memory: "512Mi"
|
||||
cpu: "500m"
|
||||
limits:
|
||||
memory: "1Gi"
|
||||
cpu: "1000m"
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health/live
|
||||
port: 5001
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health/ready
|
||||
port: 5001
|
||||
|
||||
---
|
||||
apiVersion: autoscaling/v2
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: auth-service-hpa
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
kind: Deployment
|
||||
name: auth-service
|
||||
minReplicas: 3
|
||||
maxReplicas: 10
|
||||
metrics:
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: 70
|
||||
```
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Skill Structure Template
|
||||
|
||||
Each skill will follow this structure:
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: skill-name
|
||||
description: Brief description of when to use this skill
|
||||
---
|
||||
|
||||
# Skill Title
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Clear description of scenarios where this skill applies.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
Key concepts and terminology.
|
||||
|
||||
## Patterns and Best Practices
|
||||
|
||||
### Pattern 1: Name
|
||||
|
||||
**Purpose**: What this pattern solves
|
||||
**When to Use**: Specific scenarios
|
||||
**Implementation**: Code example
|
||||
**Common Mistakes**: What to avoid
|
||||
|
||||
### Pattern 2: Name
|
||||
|
||||
[Same structure...]
|
||||
|
||||
## Code Examples
|
||||
|
||||
### Example 1: Scenario
|
||||
|
||||
Full working example with explanation.
|
||||
|
||||
### Example 2: Scenario
|
||||
|
||||
[More examples...]
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
List of anti-patterns and how to fix them.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
Common issues and solutions.
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Item 1
|
||||
- [ ] Item 2
|
||||
|
||||
## Resources
|
||||
|
||||
Links to related docs and code.
|
||||
```
|
||||
|
||||
## Content Sources
|
||||
|
||||
Each skill will extract patterns from:
|
||||
|
||||
1. **testing-patterns**:
|
||||
|
||||
- `services/_template/jest.config.ts`
|
||||
- `services/_template/src/__tests__/setupTests.ts`
|
||||
- `services/_template/src/modules/feature/__tests__/*.test.ts`
|
||||
- `services/_template/src/__tests__/*.e2e.ts`
|
||||
|
||||
2. **api-design**:
|
||||
|
||||
- `services/_template/src/modules/feature/feature.dto.ts`
|
||||
- `services/_template/src/modules/feature/feature.controller.ts`
|
||||
- `services/_template/src/modules/feature/feature.module.ts`
|
||||
- `services/_template/src/docs/swagger.ts`
|
||||
- `services/_template/src/errors/`
|
||||
|
||||
3. **database-prisma**:
|
||||
|
||||
- `services/_template/prisma/schema.prisma`
|
||||
- `services/_template/prisma/seed.ts`
|
||||
- `services/_template/src/modules/common/repository.ts`
|
||||
- `services/_template/src/modules/feature/feature.repository.ts`
|
||||
- `services/_template/src/config/database.config.ts`
|
||||
|
||||
4. **observability-monitoring**:
|
||||
|
||||
- `services/_template/src/middlewares/metrics.middleware.ts`
|
||||
- `services/_template/src/middlewares/correlation.middleware.ts`
|
||||
- `services/_template/src/middlewares/logger.middleware.ts`
|
||||
- `services/_template/src/modules/health/health.controller.ts`
|
||||
- `infra/observability/prometheus/prometheus.yml`
|
||||
|
||||
5. **deployment-kubernetes**:
|
||||
|
||||
- `deployments/production/kubernetes/auth-service.yaml`
|
||||
- `deployments/production/kubernetes/configmap.yaml`
|
||||
- `deployments/production/kubernetes/secrets.yaml.example`
|
||||
- `deployments/production/kubernetes/ingress.yaml`
|
||||
- `deployments/staging/kubernetes/` (for comparison)
|
||||
|
||||
## Skill Features
|
||||
|
||||
Each skill will include:
|
||||
|
||||
- Clear "When to Use" section
|
||||
- Real code examples from the codebase
|
||||
- Common mistakes and anti-patterns
|
||||
- Troubleshooting guide
|
||||
- Checklist for implementation
|
||||
- Links to related documentation
|
||||
- Cross-references to other skills
|
||||
|
||||
## Quality Standards
|
||||
|
||||
- Length: 400-500 lines per skill
|
||||
- Language: English only (technical content)
|
||||
- Code examples: Working, tested patterns from codebase
|
||||
- Format: Markdown with code blocks
|
||||
- Structure: Consistent across all skills
|
||||
- Practical: Focus on actionable patterns, not theory
|
||||
|
||||
## Expected Outcomes
|
||||
|
||||
After creating these skills, developers will have:
|
||||
|
||||
1. Clear testing patterns for unit, integration, and E2E tests
|
||||
2. Standardized API design with validation and documentation
|
||||
3. Database best practices with Prisma and repository pattern
|
||||
4. Observability setup for metrics, logging, and tracing
|
||||
5. Kubernetes deployment patterns for production
|
||||
|
||||
These skills will complement existing skills:
|
||||
|
||||
- `project-rules`: General architecture and standards
|
||||
- `documentation`: How to write docs
|
||||
- `comment-code`: Bilingual code comments
|
||||
|
||||
## Implementation Order
|
||||
|
||||
1. `testing-patterns` - Most frequently used
|
||||
2. `api-design` - Every new feature needs this
|
||||
3. `database-prisma` - Database work is common
|
||||
4. `observability-monitoring` - Important for debugging
|
||||
5. `deployment-kubernetes` - Less frequent but critical
|
||||
|
||||
Each skill will be self-contained and can be used independently.
|
||||
270
.cursor/plans/fix_template_structure_870b6de9.plan.md
Normal file
270
.cursor/plans/fix_template_structure_870b6de9.plan.md
Normal file
@@ -0,0 +1,270 @@
|
||||
---
|
||||
name: Fix Template Structure
|
||||
overview: Restructure services/_template to align with microservices platform architecture by removing duplicate Docker/Traefik configs and refactoring ARCHITECTURE.md to focus on single service context with platform integration.
|
||||
todos:
|
||||
- id: delete-docker-compose
|
||||
content: Delete docker-compose.yml and docker-compose.prod.yml from services/_template/
|
||||
status: completed
|
||||
- id: delete-traefik-config
|
||||
content: Delete services/_template/traefik/ directory
|
||||
status: completed
|
||||
- id: refactor-architecture-md
|
||||
content: "Refactor ARCHITECTURE.md with clear sections: Single Service Architecture, Platform Integration, Deployment Context"
|
||||
status: completed
|
||||
- id: update-readme-deployment
|
||||
content: Update README.md to remove Docker Development option and add Platform Integration section
|
||||
status: completed
|
||||
- id: update-readme-traefik
|
||||
content: Update README.md Traefik references to point to infra/traefik/ and explain Docker labels
|
||||
status: completed
|
||||
---
|
||||
|
||||
# Fix Template Structure for Microservices Platform
|
||||
|
||||
## Problem Summary
|
||||
|
||||
The `services/_template` currently contains configurations that conflict with the platform-level setup:
|
||||
|
||||
1. Docker Compose files that duplicate `deployments/local/docker-compose.yml` functionality
|
||||
2. Traefik configuration that duplicates `infra/traefik/` global config
|
||||
3. ARCHITECTURE.md that describes multi-service architecture instead of single service template
|
||||
|
||||
## Proposed Changes
|
||||
|
||||
### 1. Remove Docker Compose Files
|
||||
|
||||
**Files to delete:**
|
||||
|
||||
- [`services/_template/docker-compose.yml`](services/_template/docker-compose.yml) - Full environment setup (conflicts with deployments/local/)
|
||||
- [`services/_template/docker-compose.prod.yml`](services/_template/docker-compose.prod.yml) - Production overrides (should be in deployments/)
|
||||
|
||||
**File to keep:**
|
||||
|
||||
- [`services/_template/docker-compose.test.yml`](services/_template/docker-compose.test.yml) - Isolated test environment (useful for CI/CD)
|
||||
|
||||
**Rationale:**
|
||||
|
||||
- Template services should be deployed via `deployments/local/docker-compose.yml`
|
||||
- Individual services don't need their own compose files for orchestration
|
||||
- Test compose file is legitimate for isolated testing
|
||||
|
||||
### 2. Remove Duplicate Traefik Configuration
|
||||
|
||||
**Directory to delete:**
|
||||
|
||||
- `services/_template/traefik/` - Contains duplicate traefik.yml and dynamic.yml
|
||||
|
||||
**Rationale:**
|
||||
|
||||
- Traefik is a platform-level API Gateway managed at `infra/traefik/`
|
||||
- Service discovery happens via Docker labels in `deployments/local/docker-compose.yml`
|
||||
- Services don't configure Traefik directly; they register via labels
|
||||
|
||||
**Example of correct pattern:**
|
||||
|
||||
```yaml
|
||||
# In deployments/local/docker-compose.yml
|
||||
services:
|
||||
my-service:
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.my-service.rule=PathPrefix(`/api/v1/my-service`)"
|
||||
- "traefik.http.services.my-service.loadbalancer.server.port=5000"
|
||||
```
|
||||
|
||||
### 3. Refactor ARCHITECTURE.md
|
||||
|
||||
**Current issues:**
|
||||
|
||||
- Shows multi-service architecture (Auth, User, Product, Order, Payment)
|
||||
- Mixes single service internals with platform-level concerns
|
||||
- Unclear context about what the template represents
|
||||
|
||||
**New structure:**
|
||||
|
||||
```markdown
|
||||
# Service Template Architecture
|
||||
|
||||
## Part 1: Single Service Architecture (Internal)
|
||||
- Component layers: Controller → Service → Repository
|
||||
- Middleware chain: Correlation → Auth → Validation → Error → Logger → Metrics
|
||||
- Data flow within one service
|
||||
- Internal dependencies: Database, Redis, Jaeger client
|
||||
|
||||
## Part 2: Platform Integration (External)
|
||||
- How this service fits in the microservices platform
|
||||
- Integration with Traefik API Gateway (via Docker labels)
|
||||
- Shared infrastructure: Redis, PostgreSQL, Observability stack
|
||||
- Service discovery and registration
|
||||
- Inter-service communication patterns
|
||||
|
||||
## Part 3: Deployment Context
|
||||
- Reference to deployments/local/docker-compose.yml
|
||||
- How to add this service to the platform
|
||||
- Environment variables and configuration
|
||||
```
|
||||
|
||||
**Diagrams to update:**
|
||||
|
||||
1. **Internal Service Architecture** - Focus on single service layers
|
||||
```mermaid
|
||||
graph TD
|
||||
Request[HTTP Request] --> Traefik[Traefik Gateway]
|
||||
Traefik -->|Routes to Service| Middleware[Middleware Chain]
|
||||
|
||||
subgraph SingleService[Single Service Boundary]
|
||||
Middleware --> Correlation[Correlation ID]
|
||||
Correlation --> Auth[Authentication]
|
||||
Auth --> Validation[Validation]
|
||||
Validation --> Router[Router]
|
||||
Router --> Controller[Controller]
|
||||
Controller --> Service[Service Layer]
|
||||
Service --> Repository[Repository]
|
||||
Repository --> Database[(PostgreSQL)]
|
||||
Service --> Cache[(Redis)]
|
||||
end
|
||||
|
||||
Service -.->|Metrics| Prometheus[Prometheus]
|
||||
Service -.->|Traces| Jaeger[Jaeger]
|
||||
```
|
||||
|
||||
2. **Platform Integration** - Show how service fits in ecosystem
|
||||
```mermaid
|
||||
graph TD
|
||||
Client[Client] --> Traefik[Traefik API Gateway]
|
||||
|
||||
subgraph Platform[Microservices Platform]
|
||||
Traefik --> AuthService[Auth Service]
|
||||
Traefik --> YourService[Your Service from Template]
|
||||
Traefik --> OtherService[Other Services]
|
||||
|
||||
YourService --> SharedDB[(Shared PostgreSQL)]
|
||||
YourService --> SharedRedis[(Shared Redis)]
|
||||
|
||||
AuthService -.->|JWT Validation| YourService
|
||||
end
|
||||
|
||||
subgraph Observability[Observability Stack]
|
||||
Prometheus[Prometheus]
|
||||
Grafana[Grafana]
|
||||
Jaeger[Jaeger]
|
||||
Loki[Loki]
|
||||
end
|
||||
|
||||
YourService -.->|Metrics| Prometheus
|
||||
YourService -.->|Traces| Jaeger
|
||||
YourService -.->|Logs| Loki
|
||||
```
|
||||
|
||||
|
||||
### 4. Update README.md References
|
||||
|
||||
**Changes needed in [`services/_template/README.md`](services/_template/README.md):**
|
||||
|
||||
1. Remove "Option 2: Docker Development" section (lines 73-91)
|
||||
|
||||
- This references the deleted docker-compose.yml
|
||||
- Replace with reference to platform deployment
|
||||
|
||||
2. Update "Getting Started" section:
|
||||
|
||||
- Point to `deployments/local/docker-compose.yml` for full platform
|
||||
- Explain how to add this service to the platform
|
||||
- Keep local development instructions (without docker-compose)
|
||||
|
||||
3. Remove "Docker Compose Files" section (lines 472-476)
|
||||
|
||||
- Only docker-compose.test.yml remains
|
||||
|
||||
4. Update "Traefik API Gateway" section (lines 494+)
|
||||
|
||||
- Remove references to local traefik/ directory
|
||||
- Point to `infra/traefik/` for configuration
|
||||
- Explain Docker labels for service registration
|
||||
|
||||
**New section to add:**
|
||||
|
||||
````markdown
|
||||
## Adding This Service to the Platform
|
||||
|
||||
### 1. Add to deployments/local/docker-compose.yml
|
||||
|
||||
```yaml
|
||||
services:
|
||||
your-service:
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: services/your-service/Dockerfile
|
||||
container_name: your-service-local
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
- PORT=5002
|
||||
- DATABASE_URL=${DATABASE_URL}
|
||||
- REDIS_HOST=redis
|
||||
- JWT_SECRET=${JWT_SECRET}
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- microservices-network
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.your-service.rule=PathPrefix(`/api/v1/your-service`)"
|
||||
- "traefik.http.services.your-service.loadbalancer.server.port=5002"
|
||||
````
|
||||
|
||||
### 2. Start the Platform
|
||||
|
||||
```bash
|
||||
cd deployments/local
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### 3. Access Your Service
|
||||
|
||||
- API: http://localhost/api/v1/your-service
|
||||
- Health: http://localhost/api/v1/your-service/health
|
||||
- Docs: http://localhost/api/v1/your-service/api-docs
|
||||
|
||||
```
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. Delete `services/_template/docker-compose.yml`
|
||||
2. Delete `services/_template/docker-compose.prod.yml`
|
||||
3. Delete `services/_template/traefik/` directory
|
||||
4. Refactor `services/_template/ARCHITECTURE.md`:
|
||||
|
||||
- Add clear section headers for Single Service vs Platform Integration
|
||||
- Update diagrams to show correct context
|
||||
- Add deployment context section
|
||||
|
||||
5. Update `services/_template/README.md`:
|
||||
|
||||
- Remove Docker Development option
|
||||
- Remove Docker Compose Files section
|
||||
- Update Traefik references
|
||||
- Add "Adding This Service to the Platform" section
|
||||
|
||||
6. Keep `services/_template/docker-compose.test.yml` for isolated testing
|
||||
|
||||
## Files Affected
|
||||
|
||||
- Delete: `services/_template/docker-compose.yml`
|
||||
- Delete: `services/_template/docker-compose.prod.yml`
|
||||
- Delete: `services/_template/traefik/traefik.yml`
|
||||
- Delete: `services/_template/traefik/dynamic.yml`
|
||||
- Modify: `services/_template/ARCHITECTURE.md`
|
||||
- Modify: `services/_template/README.md`
|
||||
- Keep: `services/_template/docker-compose.test.yml`
|
||||
- Keep: `services/_template/Dockerfile`
|
||||
|
||||
## Expected Outcome
|
||||
|
||||
After these changes:
|
||||
|
||||
1. Template structure clearly represents a single microservice
|
||||
2. No confusion about deployment - services are added to `deployments/local/docker-compose.yml`
|
||||
3. Traefik configuration is centralized at `infra/traefik/`
|
||||
4. ARCHITECTURE.md clearly separates internal service architecture from platform integration
|
||||
5. Developers understand how to use the template in the microservices platform context
|
||||
125
.cursor/plans/service-template-improvements_98f188ff.plan.md
Normal file
125
.cursor/plans/service-template-improvements_98f188ff.plan.md
Normal file
@@ -0,0 +1,125 @@
|
||||
---
|
||||
name: service-template-improvements
|
||||
overview: "Make the service template production-ready: add testing, env management, DB scaffolding, auth, validation, docs and CI."
|
||||
todos: []
|
||||
---
|
||||
|
||||
# Improve Service Template (services/_template)
|
||||
|
||||
## Goal
|
||||
|
||||
Make `services/_template` production-ready and developer-friendly by adding testing infra, environment templates/validation, database scaffolding, auth & RBAC, request validation, API docs, standardized error handling, repository pattern, and CI. Changes should preserve existing structure and follow GoodGo project rules.
|
||||
|
||||
## High-level steps
|
||||
|
||||
1. Testing & CI
|
||||
|
||||
- Add `jest.config.ts`, `setupTests.ts`, and example unit/integration tests for `FeatureService` and `HealthController`.
|
||||
- Add `supertest` based E2E test for `/health` and `/api/v1/features`.
|
||||
- Update `package.json` scripts to run tests and coverage.
|
||||
- Files: `services/_template/package.json`, add `jest.config.ts` at repo root of template, tests under `services/_template/src/__tests__/`.
|
||||
|
||||
2. Environment & Local Dev
|
||||
|
||||
- Add `.env.example` and `.env.local.example` with all required vars (PORT, NODE_ENV, DATABASE_URL, REDIS_URL, JAEGER_ENDPOINT, TRACING_ENABLED, SERVICE_NAME, API_VERSION).
|
||||
- Enhance `src/config/app.config.ts` to accept `.env.local` overrides and document each var in `README.md`.
|
||||
- Files: `services/_template/.env.example`, `services/_template/.env.local.example`, `services/_template/src/config/app.config.ts`, `services/_template/README.md`.
|
||||
|
||||
3. Prisma & Database
|
||||
|
||||
- Add `prisma/schema.prisma` example with a `Feature` model, and `prisma/seed.ts` placeholder.
|
||||
- Add migration/dev workflow to README and `package.json` scripts (`prisma:generate`, `prisma:migrate`, `prisma:seed`).
|
||||
- Files: `services/_template/prisma/schema.prisma`, `services/_template/prisma/seed.ts`, `services/_template/package.json` (scripts already partly present — document usage).
|
||||
|
||||
4. Authentication & Authorization
|
||||
|
||||
- Add auth middleware `src/middlewares/auth.middleware.ts` using `@goodgo/auth-sdk` and JWT guards.
|
||||
- Add role-based decorator/utility and example protected route in `Feature` module.
|
||||
- Files: `services/_template/src/middlewares/auth.middleware.ts`, update `services/_template/src/routes/index.ts` and `services/_template/src/modules/feature/feature.module.ts`.
|
||||
|
||||
5. Request Validation & Sanitization
|
||||
|
||||
- Add `validateDto` middleware that uses Zod DTOs (e.g., `createFeatureDtoSchema`) and integrates in `feature.module` routes.
|
||||
- Sanitize input (simple trimming) helper.
|
||||
- Files: `services/_template/src/middlewares/validation.middleware.ts`, update `src/modules/feature/feature.module.ts` to parse body.
|
||||
|
||||
6. API Documentation (OpenAPI)
|
||||
|
||||
- Add OpenAPI spec generator (e.g., `openapi-typescript` or `swagger-jsdoc`) and `/docs` route serving Swagger UI.
|
||||
- Files: `services/_template/src/docs/openapi.ts`, `services/_template/src/routes/docs.ts`, update `README.md`.
|
||||
|
||||
7. Error Handling & Standard Errors
|
||||
|
||||
- Introduce custom error classes (`src/errors/http-error.ts`, `src/errors/not-found.ts`, `src/errors/bad-request.ts`) and an `error-code` enum.
|
||||
- Ensure `error.middleware.ts` maps known errors to proper status codes and structured `ApiResponse`.
|
||||
- Files: `services/_template/src/errors/*.ts`, update `src/middlewares/error.middleware.ts`.
|
||||
|
||||
8. Repository Pattern & Transactions
|
||||
|
||||
- Add `src/modules/common/repository.ts` scaffolding and update `FeatureService` to use repository and Prisma transactions for multi-step operations.
|
||||
- Files: `services/_template/src/modules/common/repository.ts`, update `services/_template/src/modules/feature/feature.service.ts`.
|
||||
|
||||
9. Observability Improvements
|
||||
|
||||
- Ensure metrics registry reset in tests to avoid global state contamination.
|
||||
- Add correlation id middleware and attach trace ids to logs.
|
||||
- Files: `services/_template/src/middlewares/correlation.middleware.ts`, update `src/middlewares/logger.middleware.ts` and `src/main.ts` to add header mappings.
|
||||
|
||||
10. Docker & Image Best Practices
|
||||
|
||||
- Add `.dockerignore` and ensure `NODE_ENV` build-time handling.
|
||||
- Make Docker build cache-friendly and document image tagging conventions in README.
|
||||
- Files: `services/_template/.dockerignore`, `services/_template/Dockerfile` (reviewed changes only documented).
|
||||
|
||||
11. Documentation & Examples
|
||||
|
||||
- Update `README.md` with examples: local dev, running tests, generating Prisma client, environment explanation, and how to create a new service from template.
|
||||
- Add code comment examples showing bilingual comment pattern where new modules are introduced.
|
||||
|
||||
## Implementation details & snippets
|
||||
|
||||
- Example: Add DTO validation middleware and usage:
|
||||
```typescript
|
||||
// services/_template/src/middlewares/validation.middleware.ts
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { AnyZodObject } from 'zod';
|
||||
|
||||
export const validateDto = (schema: AnyZodObject) => (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
req.body = schema.parse(req.body);
|
||||
return next();
|
||||
} catch (err: any) {
|
||||
return res.status(400).json({ success: false, error: { code: 'VALIDATION_ERROR', message: err.message } });
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
- Example: Custom HTTP error mapping in `error.middleware.ts` — convert `HttpError` to proper status and code.
|
||||
|
||||
- Tests: Add `src/__tests__/health.e2e.ts` using `supertest` to assert readiness/liveness and metrics endpoint.
|
||||
|
||||
## Todos (for tracking)
|
||||
|
||||
- setup-tests: Add Jest config, test utilities, and example tests.
|
||||
- add-dotenv-examples: Add `.env.example` and `.env.local.example` and document envs in README.
|
||||
- prisma-scaffold: Add `prisma/schema.prisma` and `prisma/seed.ts` with Feature model.
|
||||
- auth-middleware: Implement auth middleware & RBAC utilities and protect example route.
|
||||
- validation-middleware: Implement Zod validation middleware and apply to feature routes.
|
||||
- openapi: Add OpenAPI generation and Swagger UI route.
|
||||
- errors: Implement custom error classes and improve `error.middleware` mapping.
|
||||
- repository-pattern: Add repository scaffolding and update FeatureService.
|
||||
- observability: Add correlation id middleware and test-safe prom-client usage.
|
||||
- docker-ci: Add .dockerignore and document Docker improvements in README.
|
||||
- docs: Update README and ARCHITECTURE.md with new instructions.
|
||||
|
||||
Each todo will contain detailed substeps when started.
|
||||
|
||||
## Time estimates (rough)
|
||||
|
||||
- High priority set (tests, env, prisma, validation, auth): 2–4 days
|
||||
- Medium (openapi, errors, repo pattern): 1–2 days
|
||||
- Low (docker polish, extra docs): 0.5–1 day
|
||||
|
||||
## Next step
|
||||
|
||||
If you approve, I will generate a concrete implementation plan and break the top-priority todos into file-level patches. If you prefer a different priority (e.g., focus on Auth first), tell me which to prioritize.
|
||||
485
.cursor/skills/api-design/SKILL.md
Normal file
485
.cursor/skills/api-design/SKILL.md
Normal file
@@ -0,0 +1,485 @@
|
||||
---
|
||||
name: api-design
|
||||
description: RESTful API design standards for GoodGo microservices. Use when creating new API endpoints, designing DTOs, implementing controllers, writing OpenAPI documentation, or standardizing API responses.
|
||||
---
|
||||
|
||||
# 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
|
||||
- Designing resource relationships
|
||||
|
||||
## 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}
|
||||
|
||||
Examples:
|
||||
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
|
||||
POST /v1/users/123/orders # Create order for user
|
||||
```
|
||||
|
||||
## 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
|
||||
|
||||
### Success Response
|
||||
|
||||
```typescript
|
||||
interface SuccessResponse<T> {
|
||||
success: true;
|
||||
data: T;
|
||||
metadata?: {
|
||||
timestamp: string;
|
||||
version: string;
|
||||
requestId: string;
|
||||
};
|
||||
pagination?: {
|
||||
page: number;
|
||||
limit: number;
|
||||
total: number;
|
||||
totalPages: number;
|
||||
};
|
||||
}
|
||||
|
||||
// Example
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": "123",
|
||||
"email": "user@example.com",
|
||||
"name": "John Doe"
|
||||
},
|
||||
"metadata": {
|
||||
"timestamp": "2024-01-01T00:00:00Z",
|
||||
"version": "1.0.0",
|
||||
"requestId": "req_abc123"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Error Response
|
||||
|
||||
```typescript
|
||||
interface ErrorResponse {
|
||||
success: false;
|
||||
error: {
|
||||
code: string;
|
||||
message: string;
|
||||
details?: any;
|
||||
field?: string;
|
||||
stack?: string; // Only in development
|
||||
};
|
||||
metadata?: {
|
||||
timestamp: string;
|
||||
requestId: string;
|
||||
};
|
||||
}
|
||||
|
||||
// Example
|
||||
{
|
||||
"success": false,
|
||||
"error": {
|
||||
"code": "VALIDATION_ERROR",
|
||||
"message": "Invalid email format",
|
||||
"field": "email",
|
||||
"details": {
|
||||
"provided": "invalid-email",
|
||||
"expected": "valid email address"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Status Codes
|
||||
|
||||
```typescript
|
||||
// Success codes
|
||||
200 OK // GET, PUT, PATCH success
|
||||
201 Created // POST success with resource creation
|
||||
204 No Content // DELETE success
|
||||
|
||||
// Client errors
|
||||
400 Bad Request // Invalid request data
|
||||
401 Unauthorized // Missing/invalid authentication
|
||||
403 Forbidden // Valid auth but no permission
|
||||
404 Not Found // Resource doesn't exist
|
||||
409 Conflict // Resource conflict (duplicate)
|
||||
422 Unprocessable // Validation errors
|
||||
|
||||
// Server errors
|
||||
500 Internal Error // Unexpected server error
|
||||
502 Bad Gateway // External service error
|
||||
503 Service Unavailable // Service temporarily down
|
||||
504 Gateway Timeout // External service timeout
|
||||
```
|
||||
|
||||
## DTOs (Data Transfer Objects)
|
||||
|
||||
### Request DTOs
|
||||
|
||||
```typescript
|
||||
// create.dto.ts
|
||||
import { IsEmail, IsNotEmpty, IsOptional, MinLength } from 'class-validator';
|
||||
|
||||
export class CreateUserDto {
|
||||
@IsEmail()
|
||||
@IsNotEmpty()
|
||||
email: string;
|
||||
|
||||
@MinLength(6)
|
||||
@IsNotEmpty()
|
||||
password: string;
|
||||
|
||||
@IsOptional()
|
||||
name?: string;
|
||||
}
|
||||
|
||||
// update.dto.ts
|
||||
export class UpdateUserDto {
|
||||
@IsEmail()
|
||||
@IsOptional()
|
||||
email?: string;
|
||||
|
||||
@IsOptional()
|
||||
name?: string;
|
||||
|
||||
@IsOptional()
|
||||
avatar?: string;
|
||||
}
|
||||
|
||||
// query.dto.ts
|
||||
export class QueryUsersDto {
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@Min(1)
|
||||
page?: number = 1;
|
||||
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@Min(1)
|
||||
@Max(100)
|
||||
limit?: number = 10;
|
||||
|
||||
@IsOptional()
|
||||
search?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(['createdAt', 'name', 'email'])
|
||||
sortBy?: string = 'createdAt';
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(['asc', 'desc'])
|
||||
order?: 'asc' | 'desc' = 'desc';
|
||||
}
|
||||
```
|
||||
|
||||
### Response DTOs
|
||||
|
||||
```typescript
|
||||
// user.response.dto.ts
|
||||
export class UserResponseDto {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
avatar?: string;
|
||||
role: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
|
||||
// Hide sensitive data
|
||||
static fromEntity(user: User): UserResponseDto {
|
||||
const { password, ...data } = user;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
// paginated.response.dto.ts
|
||||
export class PaginatedResponseDto<T> {
|
||||
data: T[];
|
||||
pagination: {
|
||||
page: number;
|
||||
limit: number;
|
||||
total: number;
|
||||
totalPages: number;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Controller Implementation
|
||||
|
||||
```typescript
|
||||
// user.controller.ts
|
||||
@Controller('users')
|
||||
@ApiTags('Users')
|
||||
export class UserController {
|
||||
constructor(private readonly userService: UserService) {}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: 'List users' })
|
||||
@ApiQuery({ type: QueryUsersDto })
|
||||
@ApiResponse({ status: 200, type: PaginatedResponseDto })
|
||||
async list(@Query() query: QueryUsersDto): Promise<ResponseDto> {
|
||||
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')
|
||||
@ApiOperation({ summary: 'Get user by ID' })
|
||||
@ApiParam({ name: 'id', type: 'string' })
|
||||
@ApiResponse({ status: 200, type: UserResponseDto })
|
||||
@ApiResponse({ status: 404, description: 'User not found' })
|
||||
async getById(@Param('id') id: string): Promise<ResponseDto> {
|
||||
const user = await this.userService.findById(id);
|
||||
|
||||
if (!user) {
|
||||
throw new HttpException(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
code: 'USER_NOT_FOUND',
|
||||
message: `User with ID ${id} not found`
|
||||
}
|
||||
},
|
||||
HttpStatus.NOT_FOUND
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: UserResponseDto.fromEntity(user)
|
||||
};
|
||||
}
|
||||
|
||||
@Post()
|
||||
@ApiOperation({ summary: 'Create user' })
|
||||
@ApiBody({ type: CreateUserDto })
|
||||
@ApiResponse({ status: 201, type: UserResponseDto })
|
||||
async create(@Body() dto: CreateUserDto): Promise<ResponseDto> {
|
||||
const user = await this.userService.create(dto);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: UserResponseDto.fromEntity(user)
|
||||
};
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
@ApiOperation({ summary: 'Update user' })
|
||||
@UseGuards(AuthGuard)
|
||||
async update(
|
||||
@Param('id') id: string,
|
||||
@Body() dto: UpdateUserDto
|
||||
): Promise<ResponseDto> {
|
||||
const user = await this.userService.update(id, dto);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: UserResponseDto.fromEntity(user)
|
||||
};
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@ApiOperation({ summary: 'Delete user' })
|
||||
@UseGuards(AuthGuard, RolesGuard)
|
||||
@Roles('admin')
|
||||
async delete(@Param('id') id: string): Promise<ResponseDto> {
|
||||
await this.userService.delete(id);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: { deleted: true }
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## OpenAPI/Swagger Documentation
|
||||
|
||||
```yaml
|
||||
# openapi/user-service.yaml
|
||||
openapi: 3.0.0
|
||||
info:
|
||||
title: User Service API
|
||||
version: 1.0.0
|
||||
description: User management endpoints
|
||||
servers:
|
||||
- url: https://api.goodgo.com/v1
|
||||
paths:
|
||||
/users:
|
||||
get:
|
||||
summary: List users
|
||||
parameters:
|
||||
- name: page
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 1
|
||||
- name: limit
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 10
|
||||
responses:
|
||||
'200':
|
||||
description: List of users
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UserListResponse'
|
||||
post:
|
||||
summary: Create user
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/CreateUserRequest'
|
||||
responses:
|
||||
'201':
|
||||
description: User created
|
||||
'400':
|
||||
description: Validation error
|
||||
```
|
||||
|
||||
## Pagination Pattern
|
||||
|
||||
```typescript
|
||||
// pagination.service.ts
|
||||
export class PaginationService {
|
||||
paginate<T>(
|
||||
query: any,
|
||||
options: {
|
||||
page: number;
|
||||
limit: number;
|
||||
sortBy?: string;
|
||||
order?: 'asc' | 'desc';
|
||||
}
|
||||
) {
|
||||
const skip = (options.page - 1) * options.limit;
|
||||
|
||||
return {
|
||||
skip,
|
||||
take: options.limit,
|
||||
orderBy: options.sortBy ? {
|
||||
[options.sortBy]: options.order || 'desc'
|
||||
} : undefined
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
```typescript
|
||||
// error.middleware.ts
|
||||
export function errorHandler(
|
||||
err: Error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const isDev = process.env.NODE_ENV === 'development';
|
||||
|
||||
// Known errors
|
||||
if (err instanceof ValidationError) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: {
|
||||
code: 'VALIDATION_ERROR',
|
||||
message: err.message,
|
||||
details: err.errors
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (err instanceof UnauthorizedError) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
error: {
|
||||
code: 'UNAUTHORIZED',
|
||||
message: 'Authentication required'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Unknown errors
|
||||
logger.error('Unhandled error:', err);
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: {
|
||||
code: 'INTERNAL_ERROR',
|
||||
message: isDev ? err.message : 'Internal server error',
|
||||
stack: isDev ? err.stack : undefined
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Resource Naming**
|
||||
- Use plural nouns (`/users` not `/user`)
|
||||
- Use kebab-case for multi-word resources
|
||||
- Keep URLs as short as possible
|
||||
|
||||
2. **Versioning**
|
||||
- Include version in URL (`/v1/users`)
|
||||
- Maintain backward compatibility
|
||||
- Deprecate old versions gracefully
|
||||
|
||||
3. **Security**
|
||||
- Always use HTTPS
|
||||
- Implement rate limiting
|
||||
- Validate all inputs
|
||||
- Use proper authentication/authorization
|
||||
|
||||
4. **Performance**
|
||||
- Implement pagination for lists
|
||||
- Use field filtering when possible
|
||||
- Cache responses appropriately
|
||||
- Compress responses (gzip)
|
||||
|
||||
5. **Documentation**
|
||||
- Keep OpenAPI spec up to date
|
||||
- Include examples in documentation
|
||||
- Document error responses
|
||||
- Version your documentation
|
||||
@@ -43,11 +43,9 @@ async function login(email: string, password: string): Promise<string> {
|
||||
}
|
||||
```
|
||||
|
||||
## Language-Specific Patterns
|
||||
## Core Comment Patterns
|
||||
|
||||
### TypeScript/JavaScript
|
||||
|
||||
#### Function Documentation
|
||||
### Function Documentation
|
||||
```typescript
|
||||
/**
|
||||
* EN: Calculates the total price including tax and discount
|
||||
@@ -75,7 +73,7 @@ function calculateTotal(
|
||||
}
|
||||
```
|
||||
|
||||
#### Class Documentation
|
||||
### Class Documentation
|
||||
```typescript
|
||||
/**
|
||||
* EN: Handles user authentication and authorization
|
||||
@@ -88,16 +86,6 @@ export class AuthService {
|
||||
*/
|
||||
private readonly jwtSecret: string;
|
||||
|
||||
/**
|
||||
* EN: Initialize auth service with configuration
|
||||
* VI: Khởi tạo service xác thực với cấu hình
|
||||
*
|
||||
* @param config - Authentication configuration / Cấu hình xác thực
|
||||
*/
|
||||
constructor(config: AuthConfig) {
|
||||
this.jwtSecret = config.jwtSecret;
|
||||
}
|
||||
|
||||
/**
|
||||
* EN: Verify JWT token and return user payload
|
||||
* VI: Xác minh JWT token và trả về thông tin người dùng
|
||||
@@ -112,7 +100,7 @@ export class AuthService {
|
||||
}
|
||||
```
|
||||
|
||||
#### Interface/Type Documentation
|
||||
### Interface/Type Documentation
|
||||
```typescript
|
||||
/**
|
||||
* EN: User data transfer object
|
||||
@@ -133,47 +121,7 @@ interface UserDto {
|
||||
}
|
||||
```
|
||||
|
||||
#### Complex Logic Comments
|
||||
```typescript
|
||||
async function processPayment(order: Order): Promise<PaymentResult> {
|
||||
// EN: Step 1: Validate order data
|
||||
// VI: Bước 1: Xác thực dữ liệu đơn hàng
|
||||
if (!order.items.length) {
|
||||
throw new Error('Order must have items / Đơn hàng phải có sản phẩm');
|
||||
}
|
||||
|
||||
// EN: Step 2: Calculate total amount
|
||||
// VI: Bước 2: Tính tổng số tiền
|
||||
const total = order.items.reduce((sum, item) => {
|
||||
return sum + (item.price * item.quantity);
|
||||
}, 0);
|
||||
|
||||
// EN: Step 3: Process payment through gateway
|
||||
// VI: Bước 3: Xử lý thanh toán qua cổng thanh toán
|
||||
try {
|
||||
const result = await paymentGateway.charge({
|
||||
amount: total,
|
||||
currency: 'VND',
|
||||
orderId: order.id,
|
||||
});
|
||||
|
||||
// EN: Step 4: Update order status on success
|
||||
// VI: Bước 4: Cập nhật trạng thái đơn hàng khi thành công
|
||||
await updateOrderStatus(order.id, 'paid');
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
// EN: Log error and mark order as failed
|
||||
// VI: Ghi log lỗi và đánh dấu đơn hàng thất bại
|
||||
logger.error('Payment failed', { orderId: order.id, error });
|
||||
await updateOrderStatus(order.id, 'failed');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### React/Next.js Components
|
||||
|
||||
### React Components
|
||||
```typescript
|
||||
/**
|
||||
* EN: User profile card component
|
||||
@@ -187,20 +135,6 @@ export function UserCard({ user, onEdit }: UserCardProps) {
|
||||
// VI: State cục bộ cho trạng thái loading
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
/**
|
||||
* EN: Handle user profile update
|
||||
* VI: Xử lý cập nhật hồ sơ người dùng
|
||||
*/
|
||||
const handleUpdate = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await updateUser(user.id, user);
|
||||
onEdit?.();
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="user-card">
|
||||
{/* EN: Display user avatar / VI: Hiển thị avatar người dùng */}
|
||||
@@ -217,79 +151,33 @@ export function UserCard({ user, onEdit }: UserCardProps) {
|
||||
```
|
||||
|
||||
### 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
|
||||
/// 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)
|
||||
/// 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
|
||||
/// EN: Hashed password / VI: Mật khẩu đã mã hóa
|
||||
password String
|
||||
|
||||
/// EN: Display name
|
||||
/// VI: Tên hiển thị
|
||||
/// 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
|
||||
/// 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
|
||||
/// EN: Last update timestamp / VI: Thời gian cập nhật cuối
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@map("users")
|
||||
}
|
||||
```
|
||||
|
||||
### Configuration Files
|
||||
|
||||
```typescript
|
||||
// config/database.config.ts
|
||||
|
||||
/**
|
||||
* EN: Database configuration for Prisma and Neon PostgreSQL
|
||||
* VI: Cấu hình database cho Prisma và Neon PostgreSQL
|
||||
*/
|
||||
export const databaseConfig = {
|
||||
/**
|
||||
* EN: Database connection URL from environment
|
||||
* VI: URL kết nối database từ biến môi trường
|
||||
*/
|
||||
url: process.env.DATABASE_URL,
|
||||
|
||||
/**
|
||||
* EN: Connection pool settings
|
||||
* VI: Cài đặt connection pool
|
||||
*/
|
||||
pool: {
|
||||
// EN: Minimum connections in pool
|
||||
// VI: Số kết nối tối thiểu trong pool
|
||||
min: 2,
|
||||
|
||||
// EN: Maximum connections in pool
|
||||
// VI: Số kết nối tối đa trong pool
|
||||
max: 10,
|
||||
},
|
||||
|
||||
/**
|
||||
* EN: Enable query logging in development
|
||||
* VI: Bật ghi log truy vấn trong môi trường phát triển
|
||||
*/
|
||||
logging: process.env.NODE_ENV === 'development',
|
||||
};
|
||||
```
|
||||
|
||||
### API Routes/Controllers
|
||||
|
||||
### API Controllers
|
||||
```typescript
|
||||
/**
|
||||
* EN: User management controller
|
||||
@@ -312,8 +200,6 @@ export class UserController {
|
||||
// VI: Lấy người dùng từ database
|
||||
const user = await this.userService.findById(id);
|
||||
|
||||
// EN: Return 404 if user not found
|
||||
// VI: Trả về 404 nếu không tìm thấy người dùng
|
||||
if (!user) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
@@ -324,15 +210,11 @@ export class UserController {
|
||||
});
|
||||
}
|
||||
|
||||
// EN: Return user data
|
||||
// VI: Trả về dữ liệu người dùng
|
||||
return res.json({
|
||||
success: true,
|
||||
data: user,
|
||||
});
|
||||
} catch (error) {
|
||||
// EN: Handle unexpected errors
|
||||
// VI: Xử lý lỗi không mong đợi
|
||||
logger.error('Failed to get user', { error, userId: req.params.id });
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
@@ -347,7 +229,6 @@ export class UserController {
|
||||
```
|
||||
|
||||
### Middleware
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* EN: Authentication middleware to verify JWT tokens
|
||||
@@ -363,8 +244,6 @@ export function authMiddleware(
|
||||
const authHeader = req.headers.authorization;
|
||||
const token = authHeader?.replace('Bearer ', '');
|
||||
|
||||
// EN: Return 401 if no token provided
|
||||
// VI: Trả về 401 nếu không có token
|
||||
if (!token) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
@@ -379,15 +258,9 @@ export function authMiddleware(
|
||||
// EN: Verify token and extract payload
|
||||
// VI: Xác minh token và lấy payload
|
||||
const payload = jwt.verify(token, JWT_SECRET);
|
||||
|
||||
// EN: Attach user info to request
|
||||
// VI: Gắn thông tin người dùng vào request
|
||||
req.user = payload;
|
||||
|
||||
next();
|
||||
} catch (error) {
|
||||
// EN: Return 401 if token invalid or expired
|
||||
// VI: Trả về 401 nếu token không hợp lệ hoặc hết hạn
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
error: {
|
||||
@@ -459,82 +332,6 @@ export function authMiddleware(
|
||||
- Self-explanatory code
|
||||
- Standard CRUD operations
|
||||
|
||||
## Examples by Use Case
|
||||
|
||||
### Authentication Flow
|
||||
```typescript
|
||||
/**
|
||||
* EN: Complete authentication flow with refresh token
|
||||
* VI: Luồng xác thực hoàn chỉnh với refresh token
|
||||
*/
|
||||
export class AuthFlow {
|
||||
/**
|
||||
* EN: Login user and generate token pair
|
||||
* VI: Đăng nhập người dùng và tạo cặp token
|
||||
*/
|
||||
async login(credentials: LoginDto) {
|
||||
// EN: Step 1: Validate credentials
|
||||
// VI: Bước 1: Xác thực thông tin đăng nhập
|
||||
const user = await this.validateCredentials(credentials);
|
||||
|
||||
// EN: Step 2: Generate access token (15min expiry)
|
||||
// VI: Bước 2: Tạo access token (hết hạn sau 15 phút)
|
||||
const accessToken = this.generateAccessToken(user);
|
||||
|
||||
// EN: Step 3: Generate refresh token (7 days expiry)
|
||||
// VI: Bước 3: Tạo refresh token (hết hạn sau 7 ngày)
|
||||
const refreshToken = this.generateRefreshToken(user);
|
||||
|
||||
// EN: Step 4: Store refresh token in database
|
||||
// VI: Bước 4: Lưu refresh token vào database
|
||||
await this.storeRefreshToken(user.id, refreshToken);
|
||||
|
||||
return { accessToken, refreshToken };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Database Transaction
|
||||
```typescript
|
||||
/**
|
||||
* EN: Transfer money between accounts with transaction
|
||||
* VI: Chuyển tiền giữa các tài khoản với transaction
|
||||
*/
|
||||
async function transferMoney(
|
||||
fromAccountId: string,
|
||||
toAccountId: string,
|
||||
amount: number
|
||||
) {
|
||||
// EN: Use transaction to ensure atomicity
|
||||
// VI: Sử dụng transaction để đảm bảo tính nguyên tử
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
// EN: Deduct from sender account
|
||||
// VI: Trừ tiền từ tài khoản người gửi
|
||||
await tx.account.update({
|
||||
where: { id: fromAccountId },
|
||||
data: { balance: { decrement: amount } },
|
||||
});
|
||||
|
||||
// EN: Add to receiver account
|
||||
// VI: Cộng tiền vào tài khoản người nhận
|
||||
await tx.account.update({
|
||||
where: { id: toAccountId },
|
||||
data: { balance: { increment: amount } },
|
||||
});
|
||||
|
||||
// EN: Create transaction record
|
||||
// VI: Tạo bản ghi giao dịch
|
||||
return await tx.transaction.create({
|
||||
data: {
|
||||
fromAccountId,
|
||||
toAccountId,
|
||||
amount,
|
||||
type: 'TRANSFER',
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Integration with Project Rules
|
||||
|
||||
|
||||
478
.cursor/skills/database-prisma/SKILL.md
Normal file
478
.cursor/skills/database-prisma/SKILL.md
Normal file
@@ -0,0 +1,478 @@
|
||||
---
|
||||
name: database-prisma
|
||||
description: Prisma ORM and database patterns for GoodGo microservices. Use when working with databases, creating Prisma schemas, writing migrations, implementing repositories, or optimizing queries.
|
||||
---
|
||||
|
||||
# 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
|
||||
|
||||
## Prisma Setup
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
npm install @prisma/client prisma
|
||||
npm install --save-dev @types/node
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
```typescript
|
||||
// prisma/schema.prisma
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
// Base model with common fields
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
email String @unique
|
||||
name String?
|
||||
password String
|
||||
role Role @default(USER)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
// Relations
|
||||
posts Post[]
|
||||
profile Profile?
|
||||
|
||||
// Indexes for performance
|
||||
@@index([email])
|
||||
@@index([createdAt])
|
||||
@@map("users")
|
||||
}
|
||||
|
||||
model Post {
|
||||
id String @id @default(cuid())
|
||||
title String
|
||||
content String?
|
||||
published Boolean @default(false)
|
||||
authorId String
|
||||
author User @relation(fields: [authorId], references: [id])
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([authorId])
|
||||
@@index([published, createdAt])
|
||||
@@map("posts")
|
||||
}
|
||||
|
||||
model Profile {
|
||||
id String @id @default(cuid())
|
||||
bio String?
|
||||
avatar String?
|
||||
userId String @unique
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
|
||||
@@map("profiles")
|
||||
}
|
||||
|
||||
enum Role {
|
||||
USER
|
||||
ADMIN
|
||||
MODERATOR
|
||||
}
|
||||
```
|
||||
|
||||
## Database Connection
|
||||
|
||||
```typescript
|
||||
// src/lib/prisma.ts
|
||||
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', 'warn']
|
||||
: ['error'],
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
globalForPrisma.prisma = prisma;
|
||||
}
|
||||
|
||||
// Middleware for soft delete
|
||||
prisma.$use(async (params, next) => {
|
||||
if (params.model && params.action === 'delete') {
|
||||
return next({
|
||||
...params,
|
||||
action: 'update',
|
||||
args: {
|
||||
...params.args,
|
||||
data: { deletedAt: new Date() }
|
||||
}
|
||||
});
|
||||
}
|
||||
return next(params);
|
||||
});
|
||||
```
|
||||
|
||||
## Repository Pattern
|
||||
|
||||
```typescript
|
||||
// src/repositories/base.repository.ts
|
||||
export abstract class BaseRepository<T> {
|
||||
constructor(protected prisma: PrismaClient) {}
|
||||
|
||||
abstract findById(id: string): Promise<T | null>;
|
||||
abstract findAll(options?: any): Promise<T[]>;
|
||||
abstract create(data: any): Promise<T>;
|
||||
abstract update(id: string, data: any): Promise<T>;
|
||||
abstract delete(id: string): Promise<void>;
|
||||
}
|
||||
|
||||
// src/repositories/user.repository.ts
|
||||
export class UserRepository extends BaseRepository<User> {
|
||||
async findById(id: string): Promise<User | null> {
|
||||
return this.prisma.user.findUnique({
|
||||
where: { id },
|
||||
include: { profile: true }
|
||||
});
|
||||
}
|
||||
|
||||
async findByEmail(email: string): Promise<User | null> {
|
||||
return this.prisma.user.findUnique({
|
||||
where: { email }
|
||||
});
|
||||
}
|
||||
|
||||
async findAll(options: {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
search?: string;
|
||||
sortBy?: string;
|
||||
order?: 'asc' | 'desc';
|
||||
} = {}): Promise<{ data: User[]; total: number }> {
|
||||
const {
|
||||
page = 1,
|
||||
limit = 10,
|
||||
search,
|
||||
sortBy = 'createdAt',
|
||||
order = 'desc'
|
||||
} = options;
|
||||
|
||||
const where = search ? {
|
||||
OR: [
|
||||
{ email: { contains: search, mode: 'insensitive' } },
|
||||
{ name: { contains: search, mode: 'insensitive' } }
|
||||
]
|
||||
} : {};
|
||||
|
||||
const [data, total] = await Promise.all([
|
||||
this.prisma.user.findMany({
|
||||
where,
|
||||
skip: (page - 1) * limit,
|
||||
take: limit,
|
||||
orderBy: { [sortBy]: order },
|
||||
include: { profile: true }
|
||||
}),
|
||||
this.prisma.user.count({ where })
|
||||
]);
|
||||
|
||||
return { data, total };
|
||||
}
|
||||
|
||||
async create(data: CreateUserDto): Promise<User> {
|
||||
return this.prisma.user.create({
|
||||
data: {
|
||||
email: data.email,
|
||||
password: data.password,
|
||||
name: data.name,
|
||||
profile: data.bio ? {
|
||||
create: { bio: data.bio }
|
||||
} : undefined
|
||||
},
|
||||
include: { profile: true }
|
||||
});
|
||||
}
|
||||
|
||||
async update(id: string, data: UpdateUserDto): Promise<User> {
|
||||
return this.prisma.user.update({
|
||||
where: { id },
|
||||
data,
|
||||
include: { profile: true }
|
||||
});
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
await this.prisma.user.delete({
|
||||
where: { id }
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Transactions
|
||||
|
||||
```typescript
|
||||
// Transaction example
|
||||
export class TransferService {
|
||||
async transferFunds(
|
||||
fromAccountId: string,
|
||||
toAccountId: string,
|
||||
amount: number
|
||||
) {
|
||||
return await this.prisma.$transaction(async (tx) => {
|
||||
// Check balance
|
||||
const fromAccount = await tx.account.findUnique({
|
||||
where: { id: fromAccountId }
|
||||
});
|
||||
|
||||
if (!fromAccount || fromAccount.balance < amount) {
|
||||
throw new Error('Insufficient funds');
|
||||
}
|
||||
|
||||
// Deduct from sender
|
||||
const updatedFrom = await tx.account.update({
|
||||
where: { id: fromAccountId },
|
||||
data: { balance: { decrement: amount } }
|
||||
});
|
||||
|
||||
// Add to receiver
|
||||
const updatedTo = await tx.account.update({
|
||||
where: { id: toAccountId },
|
||||
data: { balance: { increment: amount } }
|
||||
});
|
||||
|
||||
// Create transaction record
|
||||
const transaction = await tx.transaction.create({
|
||||
data: {
|
||||
fromAccountId,
|
||||
toAccountId,
|
||||
amount,
|
||||
type: 'TRANSFER',
|
||||
status: 'COMPLETED'
|
||||
}
|
||||
});
|
||||
|
||||
return transaction;
|
||||
}, {
|
||||
maxWait: 5000,
|
||||
timeout: 10000,
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Migrations
|
||||
|
||||
```bash
|
||||
# Create migration
|
||||
npx prisma migrate dev --name add_user_table
|
||||
|
||||
# Apply migrations
|
||||
npx prisma migrate deploy
|
||||
|
||||
# Reset database
|
||||
npx prisma migrate reset
|
||||
|
||||
# Generate Prisma Client
|
||||
npx prisma generate
|
||||
```
|
||||
|
||||
### Migration Files
|
||||
|
||||
```sql
|
||||
-- migrations/20240101000000_add_user_table/migration.sql
|
||||
CREATE TABLE "users" (
|
||||
"id" TEXT NOT NULL,
|
||||
"email" TEXT NOT NULL,
|
||||
"name" TEXT,
|
||||
"password" TEXT NOT NULL,
|
||||
"role" TEXT NOT NULL DEFAULT 'USER',
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
|
||||
CREATE INDEX "users_createdAt_idx" ON "users"("createdAt");
|
||||
```
|
||||
|
||||
## Query Optimization
|
||||
|
||||
```typescript
|
||||
// Optimized queries
|
||||
export class OptimizedUserRepository {
|
||||
// Select only needed fields
|
||||
async findUsersLight() {
|
||||
return this.prisma.user.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Use pagination
|
||||
async findPaginated(cursor?: string) {
|
||||
return this.prisma.user.findMany({
|
||||
take: 10,
|
||||
skip: cursor ? 1 : 0,
|
||||
cursor: cursor ? { id: cursor } : undefined,
|
||||
orderBy: { createdAt: 'desc' }
|
||||
});
|
||||
}
|
||||
|
||||
// Batch operations
|
||||
async createMany(users: CreateUserDto[]) {
|
||||
return this.prisma.user.createMany({
|
||||
data: users,
|
||||
skipDuplicates: true
|
||||
});
|
||||
}
|
||||
|
||||
// Use raw SQL for complex queries
|
||||
async getStatistics() {
|
||||
return this.prisma.$queryRaw`
|
||||
SELECT
|
||||
COUNT(*) as total,
|
||||
COUNT(CASE WHEN role = 'ADMIN' THEN 1 END) as admins,
|
||||
COUNT(CASE WHEN created_at > NOW() - INTERVAL '30 days' THEN 1 END) as new_users
|
||||
FROM users
|
||||
`;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Seeding
|
||||
|
||||
```typescript
|
||||
// prisma/seed.ts
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import bcrypt from 'bcrypt';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
// Create admin user
|
||||
const adminPassword = await bcrypt.hash('admin123', 10);
|
||||
const admin = await prisma.user.upsert({
|
||||
where: { email: 'admin@goodgo.com' },
|
||||
update: {},
|
||||
create: {
|
||||
email: 'admin@goodgo.com',
|
||||
name: 'Admin User',
|
||||
password: adminPassword,
|
||||
role: 'ADMIN'
|
||||
}
|
||||
});
|
||||
|
||||
// Create test users
|
||||
const testUsers = Array.from({ length: 10 }, (_, i) => ({
|
||||
email: `user${i}@example.com`,
|
||||
name: `Test User ${i}`,
|
||||
password: bcrypt.hashSync('password123', 10)
|
||||
}));
|
||||
|
||||
await prisma.user.createMany({
|
||||
data: testUsers,
|
||||
skipDuplicates: true
|
||||
});
|
||||
|
||||
console.log('Database seeded successfully');
|
||||
}
|
||||
|
||||
main()
|
||||
.catch(console.error)
|
||||
.finally(() => prisma.$disconnect());
|
||||
```
|
||||
|
||||
## Neon PostgreSQL Configuration
|
||||
|
||||
```typescript
|
||||
// .env
|
||||
DATABASE_URL="postgresql://user:password@ep-xxx.us-east-1.aws.neon.tech/dbname?sslmode=require"
|
||||
|
||||
// Connection pooling for serverless
|
||||
DIRECT_URL="postgresql://user:password@ep-xxx.us-east-1.aws.neon.tech/dbname?sslmode=require"
|
||||
```
|
||||
|
||||
## Testing with Prisma
|
||||
|
||||
```typescript
|
||||
// __tests__/user.repository.test.ts
|
||||
import { mockDeep, mockReset } from 'jest-mock-extended';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
jest.mock('../src/lib/prisma', () => ({
|
||||
__esModule: true,
|
||||
prisma: mockDeep<PrismaClient>()
|
||||
}));
|
||||
|
||||
describe('UserRepository', () => {
|
||||
beforeEach(() => {
|
||||
mockReset(prismaMock);
|
||||
});
|
||||
|
||||
it('should create user', async () => {
|
||||
const user = { id: '1', email: 'test@example.com' };
|
||||
prismaMock.user.create.mockResolvedValue(user);
|
||||
|
||||
const result = await repository.create({
|
||||
email: 'test@example.com',
|
||||
password: 'password'
|
||||
});
|
||||
|
||||
expect(result).toEqual(user);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Schema Design**
|
||||
- Use appropriate field types
|
||||
- Add indexes for frequently queried fields
|
||||
- Use relations instead of storing JSON
|
||||
- Implement soft deletes when needed
|
||||
|
||||
2. **Performance**
|
||||
- Use select to fetch only needed fields
|
||||
- Implement pagination for large datasets
|
||||
- Use connection pooling
|
||||
- Cache frequently accessed data
|
||||
|
||||
3. **Security**
|
||||
- Never expose sensitive fields
|
||||
- Use parameterized queries
|
||||
- Validate input before database operations
|
||||
- Implement row-level security
|
||||
|
||||
4. **Maintenance**
|
||||
- Keep migrations small and focused
|
||||
- Test migrations before production
|
||||
- Backup before major changes
|
||||
- Monitor query performance
|
||||
486
.cursor/skills/deployment-kubernetes/SKILL.md
Normal file
486
.cursor/skills/deployment-kubernetes/SKILL.md
Normal file
@@ -0,0 +1,486 @@
|
||||
---
|
||||
name: deployment-kubernetes
|
||||
description: Kubernetes deployment patterns for GoodGo microservices. Use when deploying to staging/production, creating K8s manifests, configuring HPA, setting up ingress, or troubleshooting K8s deployments.
|
||||
---
|
||||
|
||||
# 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
|
||||
|
||||
## Service Deployment Manifest
|
||||
|
||||
```yaml
|
||||
# kubernetes/auth-service.yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: auth-service
|
||||
namespace: goodgo
|
||||
labels:
|
||||
app: auth-service
|
||||
version: v1
|
||||
spec:
|
||||
replicas: 3
|
||||
selector:
|
||||
matchLabels:
|
||||
app: auth-service
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: auth-service
|
||||
version: v1
|
||||
spec:
|
||||
containers:
|
||||
- name: auth-service
|
||||
image: goodgo/auth-service:latest
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- containerPort: 3000
|
||||
name: http
|
||||
env:
|
||||
- name: NODE_ENV
|
||||
value: "production"
|
||||
- name: PORT
|
||||
value: "3000"
|
||||
- name: DATABASE_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: database-secrets
|
||||
key: url
|
||||
- name: JWT_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: auth-secrets
|
||||
key: jwt-secret
|
||||
- name: REDIS_URL
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: redis-config
|
||||
key: url
|
||||
resources:
|
||||
requests:
|
||||
memory: "256Mi"
|
||||
cpu: "250m"
|
||||
limits:
|
||||
memory: "512Mi"
|
||||
cpu: "500m"
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 3000
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 10
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /ready
|
||||
port: 3000
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: auth-service
|
||||
namespace: goodgo
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app: auth-service
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 3000
|
||||
protocol: TCP
|
||||
```
|
||||
|
||||
## Horizontal Pod Autoscaler
|
||||
|
||||
```yaml
|
||||
# kubernetes/hpa.yaml
|
||||
apiVersion: autoscaling/v2
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: auth-service-hpa
|
||||
namespace: goodgo
|
||||
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
|
||||
- type: Resource
|
||||
resource:
|
||||
name: memory
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: 80
|
||||
behavior:
|
||||
scaleDown:
|
||||
stabilizationWindowSeconds: 300
|
||||
policies:
|
||||
- type: Percent
|
||||
value: 50
|
||||
periodSeconds: 60
|
||||
scaleUp:
|
||||
stabilizationWindowSeconds: 0
|
||||
policies:
|
||||
- type: Percent
|
||||
value: 100
|
||||
periodSeconds: 15
|
||||
```
|
||||
|
||||
## ConfigMap & Secrets
|
||||
|
||||
```yaml
|
||||
# kubernetes/configmap.yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: app-config
|
||||
namespace: goodgo
|
||||
data:
|
||||
NODE_ENV: "production"
|
||||
LOG_LEVEL: "info"
|
||||
REDIS_URL: "redis://redis-service:6379"
|
||||
METRICS_ENABLED: "true"
|
||||
|
||||
---
|
||||
# kubernetes/secrets.yaml (example - use sealed-secrets in production)
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: database-secrets
|
||||
namespace: goodgo
|
||||
type: Opaque
|
||||
stringData:
|
||||
url: "postgresql://user:pass@postgres:5432/db"
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: auth-secrets
|
||||
namespace: goodgo
|
||||
type: Opaque
|
||||
stringData:
|
||||
jwt-secret: "your-secret-key"
|
||||
refresh-secret: "your-refresh-secret"
|
||||
```
|
||||
|
||||
## Ingress Configuration
|
||||
|
||||
```yaml
|
||||
# kubernetes/ingress.yaml
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: api-ingress
|
||||
namespace: goodgo
|
||||
annotations:
|
||||
kubernetes.io/ingress.class: nginx
|
||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
nginx.ingress.kubernetes.io/rate-limit: "100"
|
||||
nginx.ingress.kubernetes.io/ssl-redirect: "true"
|
||||
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
|
||||
- path: /users
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: user-service
|
||||
port:
|
||||
number: 80
|
||||
```
|
||||
|
||||
## Database Deployment (Development Only)
|
||||
|
||||
```yaml
|
||||
# kubernetes/postgres.yaml
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: postgres
|
||||
namespace: goodgo
|
||||
spec:
|
||||
serviceName: postgres
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: postgres
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: postgres
|
||||
spec:
|
||||
containers:
|
||||
- name: postgres
|
||||
image: postgres:14-alpine
|
||||
ports:
|
||||
- containerPort: 5432
|
||||
env:
|
||||
- name: POSTGRES_DB
|
||||
value: goodgo
|
||||
- name: POSTGRES_USER
|
||||
value: postgres
|
||||
- name: POSTGRES_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: postgres-secret
|
||||
key: password
|
||||
volumeMounts:
|
||||
- name: postgres-storage
|
||||
mountPath: /var/lib/postgresql/data
|
||||
volumeClaimTemplates:
|
||||
- metadata:
|
||||
name: postgres-storage
|
||||
spec:
|
||||
accessModes: ["ReadWriteOnce"]
|
||||
resources:
|
||||
requests:
|
||||
storage: 10Gi
|
||||
```
|
||||
|
||||
## Deployment Scripts
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# scripts/deploy-k8s.sh
|
||||
|
||||
# Set namespace
|
||||
NAMESPACE="goodgo"
|
||||
ENVIRONMENT="${1:-staging}"
|
||||
|
||||
# Create namespace if not exists
|
||||
kubectl create namespace $NAMESPACE --dry-run=client -o yaml | kubectl apply -f -
|
||||
|
||||
# Apply configurations
|
||||
echo "Applying ConfigMaps..."
|
||||
kubectl apply -f kubernetes/configmap-$ENVIRONMENT.yaml
|
||||
|
||||
echo "Applying Secrets..."
|
||||
kubectl apply -f kubernetes/secrets-$ENVIRONMENT.yaml
|
||||
|
||||
echo "Deploying services..."
|
||||
kubectl apply -f kubernetes/auth-service.yaml
|
||||
kubectl apply -f kubernetes/user-service.yaml
|
||||
|
||||
echo "Configuring autoscaling..."
|
||||
kubectl apply -f kubernetes/hpa.yaml
|
||||
|
||||
echo "Setting up ingress..."
|
||||
kubectl apply -f kubernetes/ingress.yaml
|
||||
|
||||
# Wait for rollout
|
||||
kubectl rollout status deployment/auth-service -n $NAMESPACE
|
||||
kubectl rollout status deployment/user-service -n $NAMESPACE
|
||||
|
||||
echo "Deployment complete!"
|
||||
```
|
||||
|
||||
## Health Check Implementation
|
||||
|
||||
```typescript
|
||||
// src/modules/health/health.controller.ts
|
||||
export class HealthController {
|
||||
constructor(
|
||||
private prisma: PrismaClient,
|
||||
private redis: Redis
|
||||
) {}
|
||||
|
||||
// Liveness probe - is the service alive?
|
||||
async liveness(req: Request, res: Response) {
|
||||
res.status(200).json({ status: 'ok' });
|
||||
}
|
||||
|
||||
// Readiness probe - is the service ready to accept traffic?
|
||||
async readiness(req: Request, res: Response) {
|
||||
try {
|
||||
// Check database connection
|
||||
await this.prisma.$queryRaw`SELECT 1`;
|
||||
|
||||
// Check Redis connection
|
||||
await this.redis.ping();
|
||||
|
||||
res.status(200).json({
|
||||
status: 'ready',
|
||||
checks: {
|
||||
database: 'ok',
|
||||
redis: 'ok'
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(503).json({
|
||||
status: 'not ready',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Monitoring with Prometheus
|
||||
|
||||
```yaml
|
||||
# kubernetes/servicemonitor.yaml
|
||||
apiVersion: monitoring.coreos.com/v1
|
||||
kind: ServiceMonitor
|
||||
metadata:
|
||||
name: auth-service-monitor
|
||||
namespace: goodgo
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: auth-service
|
||||
endpoints:
|
||||
- port: http
|
||||
path: /metrics
|
||||
interval: 30s
|
||||
```
|
||||
|
||||
## Common Commands
|
||||
|
||||
```bash
|
||||
# Deploy to staging
|
||||
kubectl apply -f kubernetes/ -n goodgo-staging
|
||||
|
||||
# Check deployment status
|
||||
kubectl get deployments -n goodgo
|
||||
kubectl get pods -n goodgo
|
||||
kubectl get svc -n goodgo
|
||||
|
||||
# View logs
|
||||
kubectl logs -f deployment/auth-service -n goodgo
|
||||
kubectl logs -f pod-name -n goodgo --tail=100
|
||||
|
||||
# Scale manually
|
||||
kubectl scale deployment auth-service --replicas=5 -n goodgo
|
||||
|
||||
# Update image
|
||||
kubectl set image deployment/auth-service auth-service=goodgo/auth-service:v1.2.3 -n goodgo
|
||||
|
||||
# Rollback
|
||||
kubectl rollout undo deployment/auth-service -n goodgo
|
||||
|
||||
# Port forward for debugging
|
||||
kubectl port-forward service/auth-service 3000:80 -n goodgo
|
||||
|
||||
# Execute command in pod
|
||||
kubectl exec -it pod-name -n goodgo -- /bin/sh
|
||||
|
||||
# View HPA status
|
||||
kubectl get hpa -n goodgo
|
||||
kubectl describe hpa auth-service-hpa -n goodgo
|
||||
|
||||
# View resource usage
|
||||
kubectl top nodes
|
||||
kubectl top pods -n goodgo
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Pod Not Starting
|
||||
|
||||
```bash
|
||||
# Check pod status
|
||||
kubectl describe pod pod-name -n goodgo
|
||||
|
||||
# Check events
|
||||
kubectl get events -n goodgo --sort-by='.lastTimestamp'
|
||||
|
||||
# Check logs
|
||||
kubectl logs pod-name -n goodgo --previous
|
||||
```
|
||||
|
||||
### ImagePullBackOff
|
||||
|
||||
```bash
|
||||
# Check image name and tag
|
||||
kubectl describe pod pod-name -n goodgo | grep -i image
|
||||
|
||||
# Check image pull secrets
|
||||
kubectl get secrets -n goodgo
|
||||
```
|
||||
|
||||
### CrashLoopBackOff
|
||||
|
||||
```bash
|
||||
# Check logs of crashed container
|
||||
kubectl logs pod-name -n goodgo --previous
|
||||
|
||||
# Check resource limits
|
||||
kubectl describe pod pod-name -n goodgo | grep -A 5 Limits
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Resource Management**
|
||||
- Always set resource requests and limits
|
||||
- Monitor actual usage and adjust accordingly
|
||||
- Use HPA for automatic scaling
|
||||
|
||||
2. **Configuration**
|
||||
- Use ConfigMaps for non-sensitive config
|
||||
- Use Secrets for sensitive data
|
||||
- Never hardcode configuration in images
|
||||
|
||||
3. **Health Checks**
|
||||
- Implement both liveness and readiness probes
|
||||
- Set appropriate timeouts and thresholds
|
||||
- Include dependency checks in readiness probe
|
||||
|
||||
4. **Deployment**
|
||||
- Use rolling updates for zero-downtime
|
||||
- Set maxSurge and maxUnavailable appropriately
|
||||
- Test deployments in staging first
|
||||
|
||||
5. **Security**
|
||||
- Run containers as non-root user
|
||||
- Use network policies to restrict traffic
|
||||
- Regularly update base images
|
||||
- Use sealed-secrets or external secret manager
|
||||
|
||||
6. **Monitoring**
|
||||
- Expose metrics endpoint
|
||||
- Set up alerts for critical issues
|
||||
- Monitor resource usage and performance
|
||||
438
.cursor/skills/documentation/SKILL.md
Normal file
438
.cursor/skills/documentation/SKILL.md
Normal file
@@ -0,0 +1,438 @@
|
||||
---
|
||||
name: documentation
|
||||
description: Guidelines for writing technical documentation in the GoodGo project. Use when creating or updating README files, guides, architecture docs, or API documentation. Ensures bilingual (EN/VI) consistency and proper structure.
|
||||
---
|
||||
|
||||
# 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
|
||||
```
|
||||
491
.cursor/skills/observability-monitoring/SKILL.md
Normal file
491
.cursor/skills/observability-monitoring/SKILL.md
Normal file
@@ -0,0 +1,491 @@
|
||||
---
|
||||
name: observability-monitoring
|
||||
description: Observability and monitoring patterns for GoodGo microservices. Use when adding metrics, implementing logging, setting up tracing, creating health checks, or debugging production issues.
|
||||
---
|
||||
|
||||
# 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)
|
||||
|
||||
## Structured Logging
|
||||
|
||||
```typescript
|
||||
// src/lib/logger.ts
|
||||
import winston from 'winston';
|
||||
|
||||
const logFormat = winston.format.combine(
|
||||
winston.format.timestamp(),
|
||||
winston.format.errors({ stack: true }),
|
||||
winston.format.json()
|
||||
);
|
||||
|
||||
export const logger = winston.createLogger({
|
||||
level: process.env.LOG_LEVEL || 'info',
|
||||
format: logFormat,
|
||||
defaultMeta: {
|
||||
service: process.env.SERVICE_NAME || 'unknown',
|
||||
environment: process.env.NODE_ENV || 'development'
|
||||
},
|
||||
transports: [
|
||||
new winston.transports.Console({
|
||||
format: process.env.NODE_ENV === 'development'
|
||||
? winston.format.combine(
|
||||
winston.format.colorize(),
|
||||
winston.format.simple()
|
||||
)
|
||||
: logFormat
|
||||
}),
|
||||
// Production: Send to log aggregation service
|
||||
...(process.env.NODE_ENV === 'production'
|
||||
? [new winston.transports.Http({
|
||||
host: 'logs.example.com',
|
||||
path: '/collect',
|
||||
ssl: true
|
||||
})]
|
||||
: [])
|
||||
]
|
||||
});
|
||||
|
||||
// Request logger middleware
|
||||
export const requestLogger = (req: Request, res: Response, next: NextFunction) => {
|
||||
const start = Date.now();
|
||||
|
||||
res.on('finish', () => {
|
||||
const duration = Date.now() - start;
|
||||
|
||||
logger.info('HTTP Request', {
|
||||
method: req.method,
|
||||
url: req.url,
|
||||
status: res.statusCode,
|
||||
duration,
|
||||
ip: req.ip,
|
||||
userAgent: req.get('user-agent'),
|
||||
correlationId: req.headers['x-correlation-id']
|
||||
});
|
||||
});
|
||||
|
||||
next();
|
||||
};
|
||||
```
|
||||
|
||||
## Metrics Collection
|
||||
|
||||
```typescript
|
||||
// src/lib/metrics.ts
|
||||
import { Registry, Counter, Histogram, Gauge } from 'prom-client';
|
||||
|
||||
export const register = new Registry();
|
||||
|
||||
// HTTP metrics
|
||||
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']
|
||||
});
|
||||
|
||||
// Business metrics
|
||||
export const userRegistrations = new Counter({
|
||||
name: 'user_registrations_total',
|
||||
help: 'Total number of user registrations',
|
||||
labelNames: ['type']
|
||||
});
|
||||
|
||||
export const activeUsers = new Gauge({
|
||||
name: 'active_users',
|
||||
help: 'Number of active users',
|
||||
labelNames: ['status']
|
||||
});
|
||||
|
||||
// Register metrics
|
||||
register.registerMetric(httpRequestDuration);
|
||||
register.registerMetric(httpRequestTotal);
|
||||
register.registerMetric(userRegistrations);
|
||||
register.registerMetric(activeUsers);
|
||||
|
||||
// Metrics middleware
|
||||
export const metricsMiddleware = (req: Request, res: Response, next: NextFunction) => {
|
||||
const start = Date.now();
|
||||
|
||||
res.on('finish', () => {
|
||||
const duration = (Date.now() - start) / 1000;
|
||||
const route = req.route?.path || req.path;
|
||||
|
||||
httpRequestDuration
|
||||
.labels(req.method, route, res.statusCode.toString())
|
||||
.observe(duration);
|
||||
|
||||
httpRequestTotal
|
||||
.labels(req.method, route, res.statusCode.toString())
|
||||
.inc();
|
||||
});
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
// Metrics endpoint
|
||||
export const metricsHandler = async (req: Request, res: Response) => {
|
||||
res.set('Content-Type', register.contentType);
|
||||
res.end(await register.metrics());
|
||||
};
|
||||
```
|
||||
|
||||
## Distributed Tracing
|
||||
|
||||
```typescript
|
||||
// src/lib/tracing.ts
|
||||
import { NodeSDK } from '@opentelemetry/sdk-node';
|
||||
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
|
||||
import { Resource } from '@opentelemetry/resources';
|
||||
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
|
||||
import { JaegerExporter } from '@opentelemetry/exporter-jaeger';
|
||||
|
||||
export const initTracing = () => {
|
||||
const jaegerExporter = new JaegerExporter({
|
||||
endpoint: process.env.JAEGER_ENDPOINT || 'http://localhost:14268/api/traces',
|
||||
});
|
||||
|
||||
const sdk = new NodeSDK({
|
||||
resource: new Resource({
|
||||
[SemanticResourceAttributes.SERVICE_NAME]: process.env.SERVICE_NAME || 'unknown',
|
||||
[SemanticResourceAttributes.SERVICE_VERSION]: process.env.SERVICE_VERSION || '1.0.0',
|
||||
}),
|
||||
traceExporter: jaegerExporter,
|
||||
instrumentations: [getNodeAutoInstrumentations()]
|
||||
});
|
||||
|
||||
sdk.start();
|
||||
|
||||
process.on('SIGTERM', () => {
|
||||
sdk.shutdown()
|
||||
.then(() => console.log('Tracing terminated'))
|
||||
.catch((error) => console.log('Error terminating tracing', error))
|
||||
.finally(() => process.exit(0));
|
||||
});
|
||||
};
|
||||
|
||||
// Custom span creation
|
||||
import { trace, SpanStatusCode } from '@opentelemetry/api';
|
||||
|
||||
export const tracedOperation = async (name: string, fn: Function) => {
|
||||
const tracer = trace.getTracer('application');
|
||||
const span = tracer.startSpan(name);
|
||||
|
||||
try {
|
||||
const result = await fn();
|
||||
span.setStatus({ code: SpanStatusCode.OK });
|
||||
return result;
|
||||
} catch (error) {
|
||||
span.setStatus({
|
||||
code: SpanStatusCode.ERROR,
|
||||
message: error.message
|
||||
});
|
||||
span.recordException(error);
|
||||
throw error;
|
||||
} finally {
|
||||
span.end();
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Health Checks
|
||||
|
||||
```typescript
|
||||
// src/modules/health/health.controller.ts
|
||||
export class HealthController {
|
||||
constructor(
|
||||
private prisma: PrismaClient,
|
||||
private redis: Redis
|
||||
) {}
|
||||
|
||||
// Liveness probe - is the service running?
|
||||
async liveness(req: Request, res: Response) {
|
||||
res.json({
|
||||
status: 'ok',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
// Readiness probe - is the service ready for traffic?
|
||||
async readiness(req: Request, res: Response) {
|
||||
const checks = await this.runHealthChecks();
|
||||
const isHealthy = Object.values(checks).every(check => check.status === 'healthy');
|
||||
|
||||
res.status(isHealthy ? 200 : 503).json({
|
||||
status: isHealthy ? 'ready' : 'not ready',
|
||||
checks,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
// Detailed health check
|
||||
async health(req: Request, res: Response) {
|
||||
const checks = await this.runHealthChecks();
|
||||
const isHealthy = Object.values(checks).every(check => check.status === 'healthy');
|
||||
|
||||
res.status(isHealthy ? 200 : 503).json({
|
||||
status: isHealthy ? 'healthy' : 'unhealthy',
|
||||
version: process.env.SERVICE_VERSION || '1.0.0',
|
||||
uptime: process.uptime(),
|
||||
checks,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
private async runHealthChecks() {
|
||||
const checks: Record<string, any> = {};
|
||||
|
||||
// Database check
|
||||
try {
|
||||
const start = Date.now();
|
||||
await this.prisma.$queryRaw`SELECT 1`;
|
||||
checks.database = {
|
||||
status: 'healthy',
|
||||
responseTime: Date.now() - start
|
||||
};
|
||||
} catch (error) {
|
||||
checks.database = {
|
||||
status: 'unhealthy',
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
|
||||
// Redis check
|
||||
try {
|
||||
const start = Date.now();
|
||||
await this.redis.ping();
|
||||
checks.redis = {
|
||||
status: 'healthy',
|
||||
responseTime: Date.now() - start
|
||||
};
|
||||
} catch (error) {
|
||||
checks.redis = {
|
||||
status: 'unhealthy',
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
|
||||
// Memory check
|
||||
const memUsage = process.memoryUsage();
|
||||
checks.memory = {
|
||||
status: memUsage.heapUsed < 500 * 1024 * 1024 ? 'healthy' : 'warning',
|
||||
heapUsed: Math.round(memUsage.heapUsed / 1024 / 1024),
|
||||
heapTotal: Math.round(memUsage.heapTotal / 1024 / 1024),
|
||||
rss: Math.round(memUsage.rss / 1024 / 1024)
|
||||
};
|
||||
|
||||
return checks;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Error Tracking
|
||||
|
||||
```typescript
|
||||
// src/lib/error-tracking.ts
|
||||
import * as Sentry from '@sentry/node';
|
||||
|
||||
export const initErrorTracking = () => {
|
||||
if (process.env.SENTRY_DSN) {
|
||||
Sentry.init({
|
||||
dsn: process.env.SENTRY_DSN,
|
||||
environment: process.env.NODE_ENV,
|
||||
tracesSampleRate: 0.1,
|
||||
beforeSend(event, hint) {
|
||||
// Filter sensitive data
|
||||
if (event.request?.cookies) {
|
||||
delete event.request.cookies;
|
||||
}
|
||||
return event;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Error handler middleware
|
||||
export const errorHandler = (
|
||||
err: Error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
// Log error
|
||||
logger.error('Unhandled error', {
|
||||
error: err.message,
|
||||
stack: err.stack,
|
||||
url: req.url,
|
||||
method: req.method,
|
||||
correlationId: req.headers['x-correlation-id']
|
||||
});
|
||||
|
||||
// Report to Sentry
|
||||
Sentry.captureException(err, {
|
||||
tags: {
|
||||
service: process.env.SERVICE_NAME
|
||||
},
|
||||
user: {
|
||||
id: req.user?.id
|
||||
}
|
||||
});
|
||||
|
||||
// Send response
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: {
|
||||
code: 'INTERNAL_ERROR',
|
||||
message: process.env.NODE_ENV === 'production'
|
||||
? 'Internal server error'
|
||||
: err.message
|
||||
}
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
## Performance Monitoring
|
||||
|
||||
```typescript
|
||||
// src/middlewares/performance.middleware.ts
|
||||
export const performanceMiddleware = (req: Request, res: Response, next: NextFunction) => {
|
||||
const start = process.hrtime.bigint();
|
||||
|
||||
res.on('finish', () => {
|
||||
const end = process.hrtime.bigint();
|
||||
const duration = Number(end - start) / 1000000; // Convert to milliseconds
|
||||
|
||||
// Log slow requests
|
||||
if (duration > 1000) {
|
||||
logger.warn('Slow request detected', {
|
||||
method: req.method,
|
||||
url: req.url,
|
||||
duration,
|
||||
threshold: 1000
|
||||
});
|
||||
}
|
||||
|
||||
// Add to response header
|
||||
res.set('X-Response-Time', `${duration}ms`);
|
||||
});
|
||||
|
||||
next();
|
||||
};
|
||||
```
|
||||
|
||||
## Grafana Dashboard Config
|
||||
|
||||
```json
|
||||
{
|
||||
"dashboard": {
|
||||
"title": "Service Metrics",
|
||||
"panels": [
|
||||
{
|
||||
"title": "Request Rate",
|
||||
"targets": [{
|
||||
"expr": "rate(http_requests_total[5m])"
|
||||
}]
|
||||
},
|
||||
{
|
||||
"title": "Request Duration",
|
||||
"targets": [{
|
||||
"expr": "histogram_quantile(0.95, http_request_duration_seconds)"
|
||||
}]
|
||||
},
|
||||
{
|
||||
"title": "Error Rate",
|
||||
"targets": [{
|
||||
"expr": "rate(http_requests_total{status=~\"5..\"}[5m])"
|
||||
}]
|
||||
},
|
||||
{
|
||||
"title": "Active Users",
|
||||
"targets": [{
|
||||
"expr": "active_users"
|
||||
}]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Alerting Rules
|
||||
|
||||
```yaml
|
||||
# prometheus/alerts.yml
|
||||
groups:
|
||||
- name: service_alerts
|
||||
rules:
|
||||
- alert: HighErrorRate
|
||||
expr: rate(http_requests_total{status=~"5.."}[5m]) > 0.05
|
||||
for: 5m
|
||||
annotations:
|
||||
summary: "High error rate detected"
|
||||
description: "Error rate is above 5% for 5 minutes"
|
||||
|
||||
- alert: HighLatency
|
||||
expr: histogram_quantile(0.95, http_request_duration_seconds) > 1
|
||||
for: 5m
|
||||
annotations:
|
||||
summary: "High latency detected"
|
||||
description: "95th percentile latency is above 1s"
|
||||
|
||||
- alert: ServiceDown
|
||||
expr: up{job="service"} == 0
|
||||
for: 1m
|
||||
annotations:
|
||||
summary: "Service is down"
|
||||
description: "Service has been down for 1 minute"
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Logging**
|
||||
- Use structured logging (JSON format)
|
||||
- Include correlation IDs for request tracing
|
||||
- Log at appropriate levels (ERROR, WARN, INFO, DEBUG)
|
||||
- Avoid logging sensitive data
|
||||
|
||||
2. **Metrics**
|
||||
- Use standard metric types (Counter, Gauge, Histogram)
|
||||
- Keep cardinality low (avoid high-cardinality labels)
|
||||
- Define SLIs and SLOs for critical paths
|
||||
- Monitor business metrics, not just technical ones
|
||||
|
||||
3. **Tracing**
|
||||
- Add traces for critical operations
|
||||
- Include relevant context in spans
|
||||
- Sample appropriately to control costs
|
||||
- Use distributed tracing for microservices
|
||||
|
||||
4. **Alerting**
|
||||
- Alert on symptoms, not causes
|
||||
- Include runbook links in alerts
|
||||
- Avoid alert fatigue with proper thresholds
|
||||
- Test alerting rules regularly
|
||||
@@ -1,469 +1,250 @@
|
||||
---
|
||||
name: project-rules
|
||||
description: GoodGo Microservices Platform coding standards, architecture patterns, and development guidelines. Use when working with this monorepo to ensure code follows project conventions for services, apps, packages, infrastructure, or when making architectural decisions.
|
||||
description: GoodGo Microservices Platform coding standards and architecture patterns. Use when working with services, apps, packages, or infrastructure.
|
||||
---
|
||||
|
||||
# GoodGo Project Rules
|
||||
|
||||
## Architecture Overview
|
||||
## Architecture
|
||||
|
||||
This is an enterprise-grade microservices monorepo with:
|
||||
- **Apps**: Next.js (web-admin, web-client) + Flutter (app-admin, app-client)
|
||||
- **Services**: Node.js/TypeScript microservices with Express
|
||||
- **Packages**: Shared libraries (logger, types, http-client, auth-sdk, tracing, config)
|
||||
- **Infrastructure**: Traefik, Redis, Neon PostgreSQL, Observability stack
|
||||
**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)
|
||||
|
||||
## Tech Stack Requirements
|
||||
**Template Location**: `services/_template/` - Use as starting point for new services
|
||||
|
||||
### Frontend
|
||||
- **Web**: Next.js 14+ with App Router, TypeScript, Tailwind CSS, Zustand
|
||||
- **Mobile**: Flutter 3.x with Provider pattern
|
||||
- Use shared types from `@goodgo/types` package
|
||||
- API calls via `@goodgo/http-client`
|
||||
## Tech Stack
|
||||
|
||||
### Backend Services
|
||||
- **Runtime**: Node.js 20+, TypeScript 5+
|
||||
- **Framework**: Express with modular architecture
|
||||
- **Database**: Prisma ORM with Neon PostgreSQL
|
||||
- **Validation**: Zod for DTOs
|
||||
- **Logging**: Use `@goodgo/logger` package
|
||||
- **Tracing**: Use `@goodgo/tracing` with OpenTelemetry
|
||||
- **Auth**: JWT tokens, use `@goodgo/auth-sdk`
|
||||
**Frontend:**
|
||||
- Next.js 14+ (App Router), TypeScript, Tailwind CSS, Zustand
|
||||
- Flutter 3.x with Provider pattern
|
||||
- Use `@goodgo/types` and `@goodgo/http-client`
|
||||
|
||||
### Infrastructure
|
||||
- **API Gateway**: Traefik with path-based routing
|
||||
- **Caching**: Redis for sessions/cache
|
||||
- **Monitoring**: Prometheus + Grafana + Loki
|
||||
- **Containerization**: Docker with multi-stage builds
|
||||
**Backend:**
|
||||
- Node.js 20+, TypeScript 5+, Express
|
||||
- Prisma ORM + Neon PostgreSQL
|
||||
- Zod validation, `@goodgo/logger`, `@goodgo/tracing`, `@goodgo/auth-sdk`
|
||||
|
||||
## Code Organization Standards
|
||||
**Infrastructure:**
|
||||
- Traefik (path-based routing), Redis (cache), Prometheus + Grafana + Loki
|
||||
|
||||
### Service Structure
|
||||
```
|
||||
services/service-name/
|
||||
├── src/
|
||||
│ ├── config/ # Configuration files
|
||||
│ ├── modules/ # Feature modules
|
||||
│ │ └── feature/
|
||||
│ │ ├── feature.controller.ts
|
||||
│ │ ├── feature.service.ts
|
||||
│ │ ├── feature.dto.ts
|
||||
│ │ └── feature.module.ts
|
||||
│ ├── middlewares/ # Express middlewares
|
||||
│ ├── routes/ # Route definitions
|
||||
│ └── main.ts # Entry point
|
||||
├── prisma/
|
||||
│ ├── schema.prisma
|
||||
│ └── seed.ts
|
||||
├── Dockerfile
|
||||
├── package.json
|
||||
└── tsconfig.json
|
||||
```
|
||||
## Project Structure
|
||||
|
||||
### Package Structure
|
||||
```
|
||||
packages/package-name/
|
||||
├── src/
|
||||
│ └── index.ts # Main export
|
||||
├── package.json
|
||||
├── tsconfig.json
|
||||
└── README.md
|
||||
```
|
||||
|
||||
### App Structure
|
||||
```
|
||||
apps/web-*/
|
||||
├── src/
|
||||
│ ├── app/ # Next.js App Router
|
||||
│ ├── services/api/ # API clients
|
||||
│ └── stores/ # Zustand stores
|
||||
├── Dockerfile
|
||||
└── package.json
|
||||
```
|
||||
**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
|
||||
|
||||
### Files & Folders
|
||||
- **Services**: `kebab-case` (e.g., `auth-service`, `user-service`)
|
||||
- **Packages**: `kebab-case` (e.g., `http-client`, `auth-sdk`)
|
||||
- **Files**: `kebab-case.type.ts` (e.g., `user.controller.ts`, `auth.service.ts`)
|
||||
- **Components**: `PascalCase.tsx` for React, `snake_case.dart` for Flutter
|
||||
- **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`
|
||||
|
||||
### Code
|
||||
- **Classes**: `PascalCase` (e.g., `UserService`, `AuthController`)
|
||||
- **Functions/Methods**: `camelCase` (e.g., `getUserById`, `validateToken`)
|
||||
- **Constants**: `UPPER_SNAKE_CASE` (e.g., `JWT_SECRET`, `API_VERSION`)
|
||||
- **Interfaces/Types**: `PascalCase` with descriptive names (e.g., `UserResponse`, `LoginDto`)
|
||||
## Workflows
|
||||
|
||||
### Package Names
|
||||
- Use `@goodgo/` scope for all packages
|
||||
- Format: `@goodgo/package-name`
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Creating New Service
|
||||
1. Copy `services/_template/` as starting point
|
||||
2. Update `package.json` with correct name: `@goodgo/service-name`
|
||||
3. Add Prisma schema if database needed
|
||||
4. Create modules following modular pattern
|
||||
**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
|
||||
6. Create Dockerfile with multi-stage build
|
||||
7. Add to `turbo.json` if needed
|
||||
8. Update documentation
|
||||
|
||||
### Creating New Package
|
||||
1. Create in `packages/` directory
|
||||
2. Use TypeScript with strict mode
|
||||
3. Export from `src/index.ts`
|
||||
4. Add to `pnpm-workspace.yaml`
|
||||
5. Document usage in README.md
|
||||
6. Version with semantic versioning
|
||||
**New Package:**
|
||||
1. Create in `packages/`, export from `src/index.ts`
|
||||
2. Add to `pnpm-workspace.yaml`
|
||||
3. Use TypeScript strict mode
|
||||
|
||||
### Adding Dependencies
|
||||
**Dependencies:**
|
||||
```bash
|
||||
# Service/App dependency
|
||||
pnpm --filter @goodgo/service-name add package-name
|
||||
|
||||
# Workspace package dependency
|
||||
pnpm --filter @goodgo/service-name add @goodgo/logger
|
||||
|
||||
# Dev dependency
|
||||
pnpm --filter @goodgo/service-name add -D @types/package-name
|
||||
|
||||
# Root dependency
|
||||
pnpm add -w package-name
|
||||
pnpm --filter @goodgo/service-name add @goodgo/logger # workspace
|
||||
pnpm --filter @goodgo/service-name add -D @types/pkg # dev
|
||||
```
|
||||
|
||||
### Database Workflow
|
||||
1. Update Prisma schema in service
|
||||
2. Generate migration: `pnpm --filter @goodgo/service-name prisma migrate dev`
|
||||
3. Generate client: `pnpm --filter @goodgo/service-name prisma generate`
|
||||
4. Use Neon PostgreSQL (no local PostgreSQL needed)
|
||||
5. Connection pooling via Prisma
|
||||
**Database:**
|
||||
```bash
|
||||
pnpm --filter @goodgo/service-name prisma migrate dev
|
||||
pnpm --filter @goodgo/service-name prisma generate
|
||||
```
|
||||
|
||||
## Code Quality Standards
|
||||
## Code Standards
|
||||
|
||||
### TypeScript
|
||||
- Enable strict mode
|
||||
- No `any` types (use `unknown` if needed)
|
||||
- Define interfaces for all DTOs
|
||||
- Use Zod for runtime validation
|
||||
- Export types from `@goodgo/types` when shared
|
||||
**TypeScript:**
|
||||
- Strict mode, no `any` (use `unknown`)
|
||||
- Zod for runtime validation
|
||||
- Export shared types from `@goodgo/types`
|
||||
|
||||
### Error Handling
|
||||
- Use custom error classes
|
||||
- Implement global error middleware
|
||||
- Log errors with context using `@goodgo/logger`
|
||||
- Return consistent error responses:
|
||||
**API Responses:**
|
||||
```typescript
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
code: 'ERROR_CODE',
|
||||
message: 'Human readable message',
|
||||
details?: any
|
||||
}
|
||||
}
|
||||
// Success: { success: true, data: any }
|
||||
// Error: { success: false, error: { code, message, details? } }
|
||||
```
|
||||
|
||||
### API Response Format
|
||||
```typescript
|
||||
// Success
|
||||
{
|
||||
success: true,
|
||||
data: any
|
||||
}
|
||||
|
||||
// Error
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
code: string,
|
||||
message: string,
|
||||
details?: any
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Logging
|
||||
**Logging:**
|
||||
```typescript
|
||||
import { logger } from '@goodgo/logger';
|
||||
|
||||
logger.info('Message', { context: 'data' });
|
||||
logger.error('Error', { error, context: 'data' });
|
||||
logger.debug('Debug info', { data });
|
||||
logger.info('Message', { context });
|
||||
logger.error('Error', { error, context });
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
- Use `.env.example` as template
|
||||
- Never commit `.env` files
|
||||
- Validate env vars at startup
|
||||
- Use Zod for env validation
|
||||
- Document all env vars in README
|
||||
**Environment:**
|
||||
- Use `.env.example` template, never commit `.env`
|
||||
- Validate with Zod at startup
|
||||
- Document all vars in README
|
||||
|
||||
## Testing Standards
|
||||
## Testing
|
||||
|
||||
### Unit Tests
|
||||
- Place tests next to source: `feature.service.test.ts`
|
||||
- Use Jest as test runner
|
||||
- Mock external dependencies
|
||||
- Aim for >80% coverage
|
||||
- **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`
|
||||
|
||||
### Integration Tests
|
||||
- Test API endpoints end-to-end
|
||||
- Use test database (Neon branch)
|
||||
- Clean up test data after tests
|
||||
## Docker
|
||||
|
||||
### Test Commands
|
||||
```bash
|
||||
pnpm test # All tests
|
||||
pnpm --filter @goodgo/service-name test # Service tests
|
||||
pnpm test:coverage # With coverage
|
||||
```
|
||||
|
||||
## Docker Standards
|
||||
|
||||
### Multi-stage Builds
|
||||
**Multi-stage Build Pattern:**
|
||||
```dockerfile
|
||||
# Build stage
|
||||
FROM node:20-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY package*.json pnpm-lock.yaml ./
|
||||
RUN npm install -g pnpm && pnpm install
|
||||
COPY . .
|
||||
RUN pnpm build
|
||||
|
||||
# Production stage
|
||||
# ... build stage
|
||||
FROM node:20-alpine
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/dist ./dist
|
||||
COPY --from=builder /app/node_modules ./node_modules
|
||||
CMD ["node", "dist/main.js"]
|
||||
# ... production stage with non-root user
|
||||
```
|
||||
|
||||
### Image Naming
|
||||
- Format: `goodgo/service-name:version`
|
||||
- Use semantic versioning
|
||||
- Tag latest for production
|
||||
**Image Naming:** `goodgo/service-name:version` (semantic versioning)
|
||||
|
||||
## Git Workflow
|
||||
|
||||
### Branch Naming
|
||||
- Feature: `feature/description`
|
||||
- Bug fix: `fix/description`
|
||||
- Hotfix: `hotfix/description`
|
||||
- Release: `release/version`
|
||||
**Branches:** `feature/`, `fix/`, `hotfix/`, `release/`
|
||||
|
||||
### Commit Messages
|
||||
Follow Conventional Commits:
|
||||
**Commits:** Conventional Commits format
|
||||
```
|
||||
type(scope): subject
|
||||
|
||||
body (optional)
|
||||
|
||||
footer (optional)
|
||||
```
|
||||
|
||||
Types: `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore`
|
||||
|
||||
Examples:
|
||||
- `feat(auth): add refresh token endpoint`
|
||||
- `fix(user): resolve email validation bug`
|
||||
- `docs(readme): update installation steps`
|
||||
**PRs:** Use template, link issues, ensure CI passes, squash merge to main
|
||||
|
||||
### Pull Requests
|
||||
- Use PR template
|
||||
- Link related issues
|
||||
- Request review from team
|
||||
- Ensure CI passes
|
||||
- Squash merge to main
|
||||
## CI/CD
|
||||
|
||||
## CI/CD Standards
|
||||
**GitHub Actions:** PR (lint, test, build) → `develop` (staging) → `main` (production)
|
||||
|
||||
### GitHub Actions
|
||||
- Run on PR: lint, test, build
|
||||
- Deploy to staging on merge to `develop`
|
||||
- Deploy to production on merge to `main`
|
||||
- Use secrets for sensitive data
|
||||
**Deployment Checklist:** Tests pass, no lint errors, env vars set, migrations applied, docs updated, monitoring configured
|
||||
|
||||
### Deployment Checklist
|
||||
- [ ] All tests pass
|
||||
- [ ] No linter errors
|
||||
- [ ] Environment variables configured
|
||||
- [ ] Database migrations applied
|
||||
- [ ] Documentation updated
|
||||
- [ ] Monitoring configured
|
||||
## Security
|
||||
|
||||
## Security Guidelines
|
||||
**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
|
||||
|
||||
### Authentication
|
||||
- Use JWT with short expiry (15min access, 7d refresh)
|
||||
- Store tokens securely (httpOnly cookies for web)
|
||||
- Implement refresh token rotation
|
||||
- Use `@goodgo/auth-sdk` for consistency
|
||||
## Performance
|
||||
|
||||
### Authorization
|
||||
- Implement RBAC (Role-Based Access Control)
|
||||
- Check permissions at service level
|
||||
- Use middleware for route protection
|
||||
**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
|
||||
|
||||
### Data Protection
|
||||
- Hash passwords with bcrypt (cost factor 12)
|
||||
- Encrypt sensitive data at rest
|
||||
- Use HTTPS in production
|
||||
- Sanitize user inputs
|
||||
- Validate all DTOs with Zod
|
||||
## Observability
|
||||
|
||||
### Secrets Management
|
||||
- Use environment variables
|
||||
- Kubernetes secrets for production
|
||||
- GitHub secrets for CI/CD
|
||||
- Never hardcode secrets
|
||||
- Rotate secrets regularly
|
||||
**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
|
||||
|
||||
## Performance Guidelines
|
||||
## Documentation
|
||||
|
||||
### Backend
|
||||
- Use Redis for caching
|
||||
- Implement database connection pooling
|
||||
- Add pagination for list endpoints
|
||||
- Use database indexes
|
||||
- Implement rate limiting
|
||||
**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
|
||||
|
||||
### Frontend
|
||||
- Use Next.js Image optimization
|
||||
- Implement code splitting
|
||||
- Lazy load components
|
||||
- Use React.memo for expensive renders
|
||||
- Optimize bundle size
|
||||
## Architecture Patterns
|
||||
|
||||
### Database
|
||||
- Use Prisma query optimization
|
||||
- Add indexes for frequent queries
|
||||
- Use database transactions
|
||||
- Implement soft deletes
|
||||
- Regular backups (Neon handles this)
|
||||
**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
|
||||
|
||||
## Monitoring & Observability
|
||||
|
||||
### Metrics
|
||||
- Use Prometheus for metrics
|
||||
- Track: request count, duration, errors
|
||||
- Set up alerts for critical metrics
|
||||
|
||||
### Logging
|
||||
- Structured logging with `@goodgo/logger`
|
||||
- Include trace IDs for correlation
|
||||
- Log levels: error, warn, info, debug
|
||||
- Aggregate logs with Loki
|
||||
|
||||
### Tracing
|
||||
- Use OpenTelemetry via `@goodgo/tracing`
|
||||
- Trace cross-service requests
|
||||
- Include context in traces
|
||||
|
||||
## Documentation Standards
|
||||
|
||||
### Code Documentation
|
||||
- JSDoc for public APIs
|
||||
- Inline comments for complex logic
|
||||
- README.md for each service/package
|
||||
- Keep docs up to date
|
||||
|
||||
### API Documentation
|
||||
- OpenAPI/Swagger specs in `docs/api/openapi/`
|
||||
- Document all endpoints
|
||||
- Include request/response examples
|
||||
- Document error codes
|
||||
|
||||
### Architecture Documentation
|
||||
- System design in `docs/architecture/`
|
||||
- Service communication patterns
|
||||
- Data flow diagrams
|
||||
- Decision records (ADRs)
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Module Pattern
|
||||
**Example Module:**
|
||||
```typescript
|
||||
// feature.module.ts
|
||||
export class FeatureModule {
|
||||
controller: FeatureController;
|
||||
service: FeatureService;
|
||||
|
||||
constructor() {
|
||||
this.service = new FeatureService();
|
||||
this.controller = new FeatureController(this.service);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### DTO Pattern
|
||||
```typescript
|
||||
// feature.dto.ts
|
||||
import { z } from 'zod';
|
||||
|
||||
// DTO with Zod
|
||||
export const CreateFeatureDto = z.object({
|
||||
name: z.string().min(1),
|
||||
email: z.string().email(),
|
||||
email: z.string().email()
|
||||
});
|
||||
|
||||
export type CreateFeatureDto = z.infer<typeof CreateFeatureDto>;
|
||||
```
|
||||
|
||||
### Controller Pattern
|
||||
```typescript
|
||||
// feature.controller.ts
|
||||
// Controller
|
||||
export class FeatureController {
|
||||
constructor(private service: FeatureService) {}
|
||||
|
||||
async create(req: Request, res: Response) {
|
||||
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);
|
||||
}
|
||||
} 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 });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Service Pattern
|
||||
```typescript
|
||||
// feature.service.ts
|
||||
export class FeatureService {
|
||||
constructor(private prisma: PrismaClient) {}
|
||||
|
||||
async create(dto: CreateFeatureDto) {
|
||||
return this.prisma.feature.create({ data: dto });
|
||||
}
|
||||
}
|
||||
## 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 connection**: Verify Neon DATABASE_URL in `.env.local`
|
||||
- **Module not found**: Run `pnpm install` in root
|
||||
- **Build errors**: Clear `dist/` and rebuild
|
||||
- **Type errors**: Run `pnpm --filter @goodgo/service-name prisma generate`
|
||||
**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 Commands
|
||||
**Debug:**
|
||||
```bash
|
||||
# Check service logs
|
||||
cd deployments/local
|
||||
docker-compose logs -f service-name
|
||||
|
||||
# Check database connection
|
||||
pnpm --filter @goodgo/service-name prisma studio
|
||||
|
||||
# Rebuild containers
|
||||
docker-compose up -d --build
|
||||
|
||||
# Check running services
|
||||
docker-compose ps
|
||||
docker-compose up -d --build
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
492
.cursor/skills/testing-patterns/SKILL.md
Normal file
492
.cursor/skills/testing-patterns/SKILL.md
Normal file
@@ -0,0 +1,492 @@
|
||||
---
|
||||
name: testing-patterns
|
||||
description: Testing best practices for GoodGo microservices. Use when writing unit tests, integration tests, E2E tests, setting up Jest, mocking dependencies, or debugging test failures.
|
||||
---
|
||||
|
||||
# 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
|
||||
|
||||
1. **Unit Tests**: Test individual functions/classes in isolation
|
||||
- Location: Next to source files (`*.test.ts`)
|
||||
- Scope: Single function or class
|
||||
- Dependencies: Mocked
|
||||
- Speed: Fast (<1s per test)
|
||||
|
||||
2. **Integration Tests**: Test component interactions
|
||||
- Location: `__tests__/` directory
|
||||
- Scope: Multiple components working together
|
||||
- Dependencies: Some real, some mocked
|
||||
- Speed: Medium (1-5s per test)
|
||||
|
||||
3. **E2E Tests**: Test complete request/response cycles
|
||||
- Location: `__tests__/*.e2e.ts`
|
||||
- Scope: Full API workflow
|
||||
- Dependencies: Test database, mocked external services
|
||||
- Speed: Slow (5-10s per test)
|
||||
|
||||
## Jest Configuration
|
||||
|
||||
```typescript
|
||||
// jest.config.ts
|
||||
import type { Config } from 'jest';
|
||||
|
||||
const config: Config = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
roots: ['<rootDir>/src'],
|
||||
testMatch: [
|
||||
'**/__tests__/**/*.test.ts',
|
||||
'**/__tests__/**/*.e2e.ts',
|
||||
'**/?(*.)+(spec|test).ts'
|
||||
],
|
||||
collectCoverageFrom: [
|
||||
'src/**/*.ts',
|
||||
'!src/**/*.d.ts',
|
||||
'!src/main.ts',
|
||||
'!src/config/**/*.ts'
|
||||
],
|
||||
coverageDirectory: 'coverage',
|
||||
coverageReporters: ['text', 'lcov', 'html'],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
branches: 70,
|
||||
functions: 70,
|
||||
lines: 70,
|
||||
statements: 70
|
||||
}
|
||||
},
|
||||
setupFilesAfterEnv: ['<rootDir>/src/__tests__/setupTests.ts'],
|
||||
testTimeout: 10000,
|
||||
clearMocks: true
|
||||
};
|
||||
|
||||
export default config;
|
||||
```
|
||||
|
||||
## Setup Files
|
||||
|
||||
```typescript
|
||||
// src/__tests__/setupTests.ts
|
||||
import { mockDeep, mockReset } from 'jest-mock-extended';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
// Mock Prisma
|
||||
jest.mock('../prisma', () => ({
|
||||
__esModule: true,
|
||||
default: mockDeep<PrismaClient>()
|
||||
}));
|
||||
|
||||
// Mock Redis
|
||||
jest.mock('ioredis', () => {
|
||||
const Redis = jest.requireActual('ioredis-mock');
|
||||
return Redis;
|
||||
});
|
||||
|
||||
// Global test utilities
|
||||
global.testUtils = {
|
||||
generateId: () => `test-${Date.now()}`,
|
||||
createMockRequest: () => ({
|
||||
headers: {},
|
||||
body: {},
|
||||
query: {},
|
||||
params: {}
|
||||
}),
|
||||
createMockResponse: () => {
|
||||
const res: any = {};
|
||||
res.status = jest.fn().mockReturnValue(res);
|
||||
res.json = jest.fn().mockReturnValue(res);
|
||||
res.send = jest.fn().mockReturnValue(res);
|
||||
return res;
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
```
|
||||
|
||||
## Testing Patterns
|
||||
|
||||
### Unit Test Pattern
|
||||
|
||||
```typescript
|
||||
// feature.service.test.ts
|
||||
import { FeatureService } from './feature.service';
|
||||
import { mockDeep } from 'jest-mock-extended';
|
||||
|
||||
describe('FeatureService', () => {
|
||||
let service: FeatureService;
|
||||
let mockRepository: any;
|
||||
|
||||
beforeEach(() => {
|
||||
mockRepository = {
|
||||
findById: jest.fn(),
|
||||
create: jest.fn(),
|
||||
update: jest.fn(),
|
||||
delete: jest.fn()
|
||||
};
|
||||
service = new FeatureService(mockRepository);
|
||||
});
|
||||
|
||||
describe('findById', () => {
|
||||
it('should return feature when found', async () => {
|
||||
// Arrange
|
||||
const mockFeature = { id: '1', name: 'Test Feature' };
|
||||
mockRepository.findById.mockResolvedValue(mockFeature);
|
||||
|
||||
// Act
|
||||
const result = await service.findById('1');
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual(mockFeature);
|
||||
expect(mockRepository.findById).toHaveBeenCalledWith('1');
|
||||
});
|
||||
|
||||
it('should throw error when feature not found', async () => {
|
||||
// Arrange
|
||||
mockRepository.findById.mockResolvedValue(null);
|
||||
|
||||
// Act & Assert
|
||||
await expect(service.findById('999')).rejects.toThrow('Feature not found');
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Integration Test Pattern
|
||||
|
||||
```typescript
|
||||
// auth.middleware.test.ts
|
||||
import { authMiddleware } from '../auth.middleware';
|
||||
import { createMockRequest, createMockResponse } from '../../test-utils';
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
describe('Auth Middleware', () => {
|
||||
const mockNext = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should call next when valid token provided', async () => {
|
||||
// Arrange
|
||||
const req = createMockRequest();
|
||||
const res = createMockResponse();
|
||||
const token = jwt.sign({ userId: '123' }, 'secret');
|
||||
req.headers.authorization = `Bearer ${token}`;
|
||||
|
||||
// Act
|
||||
await authMiddleware(req, res, mockNext);
|
||||
|
||||
// Assert
|
||||
expect(mockNext).toHaveBeenCalled();
|
||||
expect(req.user).toEqual({ userId: '123' });
|
||||
});
|
||||
|
||||
it('should return 401 when no token provided', async () => {
|
||||
// Arrange
|
||||
const req = createMockRequest();
|
||||
const res = createMockResponse();
|
||||
|
||||
// Act
|
||||
await authMiddleware(req, res, mockNext);
|
||||
|
||||
// Assert
|
||||
expect(res.status).toHaveBeenCalledWith(401);
|
||||
expect(mockNext).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### E2E Test Pattern
|
||||
|
||||
```typescript
|
||||
// feature.e2e.ts
|
||||
import supertest from 'supertest';
|
||||
import { createApp } from '../app';
|
||||
import { prisma } from '../prisma';
|
||||
|
||||
describe('Feature API E2E', () => {
|
||||
let app: any;
|
||||
let request: any;
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await createApp();
|
||||
request = supertest(app);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await prisma.feature.deleteMany();
|
||||
});
|
||||
|
||||
describe('POST /api/features', () => {
|
||||
it('should create a new feature', async () => {
|
||||
// Arrange
|
||||
const featureData = {
|
||||
name: 'New Feature',
|
||||
description: 'Feature description'
|
||||
};
|
||||
|
||||
// Act
|
||||
const response = await request
|
||||
.post('/api/features')
|
||||
.set('Authorization', 'Bearer valid-token')
|
||||
.send(featureData)
|
||||
.expect(201);
|
||||
|
||||
// Assert
|
||||
expect(response.body).toMatchObject({
|
||||
success: true,
|
||||
data: {
|
||||
name: 'New Feature',
|
||||
description: 'Feature description'
|
||||
}
|
||||
});
|
||||
|
||||
const created = await prisma.feature.findFirst({
|
||||
where: { name: 'New Feature' }
|
||||
});
|
||||
expect(created).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Mocking Strategies
|
||||
|
||||
### Mock Prisma
|
||||
|
||||
```typescript
|
||||
// __mocks__/prisma.ts
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { mockDeep, DeepMockProxy } from 'jest-mock-extended';
|
||||
|
||||
export const prismaMock = mockDeep<PrismaClient>();
|
||||
|
||||
jest.mock('../src/prisma', () => ({
|
||||
__esModule: true,
|
||||
default: prismaMock,
|
||||
}));
|
||||
|
||||
// Usage in tests
|
||||
import { prismaMock } from '../__mocks__/prisma';
|
||||
|
||||
test('should create user', async () => {
|
||||
const user = { id: '1', email: 'test@example.com' };
|
||||
prismaMock.user.create.mockResolvedValue(user);
|
||||
|
||||
const result = await createUser({ email: 'test@example.com' });
|
||||
expect(result).toEqual(user);
|
||||
});
|
||||
```
|
||||
|
||||
### Mock Redis
|
||||
|
||||
```typescript
|
||||
// __mocks__/redis.ts
|
||||
import Redis from 'ioredis-mock';
|
||||
|
||||
export const redisMock = new Redis();
|
||||
|
||||
// Usage in tests
|
||||
test('should cache value', async () => {
|
||||
const cache = new CacheService(redisMock);
|
||||
await cache.set('key', 'value');
|
||||
|
||||
const result = await cache.get('key');
|
||||
expect(result).toBe('value');
|
||||
});
|
||||
```
|
||||
|
||||
### Mock External APIs
|
||||
|
||||
```typescript
|
||||
// Mock axios
|
||||
jest.mock('axios');
|
||||
import axios from 'axios';
|
||||
const mockedAxios = axios as jest.Mocked<typeof axios>;
|
||||
|
||||
test('should fetch external data', async () => {
|
||||
mockedAxios.get.mockResolvedValue({
|
||||
data: { result: 'success' }
|
||||
});
|
||||
|
||||
const result = await fetchExternalData();
|
||||
expect(result).toEqual({ result: 'success' });
|
||||
});
|
||||
```
|
||||
|
||||
## Testing Utilities
|
||||
|
||||
```typescript
|
||||
// test-utils.ts
|
||||
export class TestFactory {
|
||||
static createUser(overrides = {}) {
|
||||
return {
|
||||
id: 'test-user-1',
|
||||
email: 'test@example.com',
|
||||
name: 'Test User',
|
||||
createdAt: new Date(),
|
||||
...overrides
|
||||
};
|
||||
}
|
||||
|
||||
static createAuthToken(userId: string) {
|
||||
return jwt.sign({ userId }, 'test-secret');
|
||||
}
|
||||
|
||||
static async cleanDatabase() {
|
||||
await prisma.user.deleteMany();
|
||||
await prisma.feature.deleteMany();
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
const user = TestFactory.createUser({ name: 'Custom Name' });
|
||||
const token = TestFactory.createAuthToken(user.id);
|
||||
```
|
||||
|
||||
## Common Test Scenarios
|
||||
|
||||
### Testing Error Handling
|
||||
|
||||
```typescript
|
||||
test('should handle database errors gracefully', async () => {
|
||||
prismaMock.user.findUnique.mockRejectedValue(
|
||||
new Error('Database connection failed')
|
||||
);
|
||||
|
||||
const response = await request
|
||||
.get('/api/users/123')
|
||||
.expect(500);
|
||||
|
||||
expect(response.body).toEqual({
|
||||
success: false,
|
||||
error: {
|
||||
code: 'INTERNAL_ERROR',
|
||||
message: 'Internal server error'
|
||||
}
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Testing Validation
|
||||
|
||||
```typescript
|
||||
describe('Validation', () => {
|
||||
it('should reject invalid email', async () => {
|
||||
const response = await request
|
||||
.post('/api/auth/register')
|
||||
.send({ email: 'invalid-email', password: '123456' })
|
||||
.expect(400);
|
||||
|
||||
expect(response.body.error.code).toBe('VALIDATION_ERROR');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Testing Pagination
|
||||
|
||||
```typescript
|
||||
test('should paginate results', async () => {
|
||||
// Create test data
|
||||
const items = Array(25).fill(null).map((_, i) => ({
|
||||
id: `item-${i}`,
|
||||
name: `Item ${i}`
|
||||
}));
|
||||
|
||||
prismaMock.item.findMany.mockResolvedValue(items.slice(0, 10));
|
||||
prismaMock.item.count.mockResolvedValue(25);
|
||||
|
||||
const response = await request
|
||||
.get('/api/items?page=1&limit=10')
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toEqual({
|
||||
success: true,
|
||||
data: items.slice(0, 10),
|
||||
pagination: {
|
||||
page: 1,
|
||||
limit: 10,
|
||||
total: 25,
|
||||
totalPages: 3
|
||||
}
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Test Commands
|
||||
|
||||
```json
|
||||
// package.json
|
||||
{
|
||||
"scripts": {
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:coverage": "jest --coverage",
|
||||
"test:unit": "jest --testPathPattern=\\.test\\.ts$",
|
||||
"test:e2e": "jest --testPathPattern=\\.e2e\\.ts$",
|
||||
"test:ci": "jest --coverage --silent --maxWorkers=2"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Debugging Tests
|
||||
|
||||
### Debug with VS Code
|
||||
|
||||
```json
|
||||
// .vscode/launch.json
|
||||
{
|
||||
"configurations": [
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Debug Jest Tests",
|
||||
"runtimeExecutable": "npm",
|
||||
"runtimeArgs": ["test", "--", "--runInBand"],
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 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
|
||||
5. Use debugger breakpoints in VS Code
|
||||
|
||||
## Best Practices Checklist
|
||||
|
||||
- [ ] 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
|
||||
- [ ] Run tests before committing
|
||||
- [ ] Keep test data realistic
|
||||
- [ ] Clean up after tests
|
||||
Reference in New Issue
Block a user