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:
Ho Ngoc Hai
2025-12-27 13:54:09 +07:00
parent 9cd074acf1
commit ab44954bd6
62 changed files with 12559 additions and 891 deletions

View 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.

View 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

View 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): 24 days
- Medium (openapi, errors, repo pattern): 12 days
- Low (docker polish, extra docs): 0.51 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.

View 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

View File

@@ -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

View 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

View 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

View 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
```

View 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

View File

@@ -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

View 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