返回 Skill 列表
extension
分类: 开发与工程无需 API Key

shared-libraries-guidelines

Quantum Skincare的TypeScript共享代码(类型、验证、工具函数)的共享库指南。涵盖类型定义、Zod验证模式、实用函数、合约维护、数据库与应用程序类型分离、枚举模式、可重用细化以及跨项目一致性。在使用@quantum/shared-types, @quantum/shared-validation, @quantum/shared-utils或维护共享合约时参考本指南。

person作者: jakexiaohubgithub

Shared Libraries Guidelines - Quantum Skincare

Purpose

Quick reference for Quantum Skincare's shared TypeScript libraries that provide types, validation schemas, and utilities across all applications (frontend, backend, data-access).

When to Use This Skill

  • Creating or modifying shared types
  • Creating or modifying Zod validation schemas
  • Working with shared utility functions
  • Maintaining API contracts across apps
  • Defining DB vs App type separations
  • Creating reusable enum patterns
  • Adding cross-project constants
  • Updating validation schemas
  • Ensuring type/validation consistency

Quick Start

New Type Checklist

  • [ ] Add to appropriate file in libs/shared/types/src/lib/
  • [ ] Export from libs/shared/types/src/index.ts
  • [ ] Document with JSDoc comments
  • [ ] Separate DB types (suffixed with DB) from App types
  • [ ] Use consistent naming conventions
  • [ ] Build library: pnpm nx build shared-types
  • [ ] Update consuming apps if breaking change

New Validation Schema Checklist

  • [ ] Add to libs/shared/validation/src/lib/validation.ts
  • [ ] Export from libs/shared/validation/src/index.ts
  • [ ] Use consistent Zod patterns
  • [ ] Add custom error messages
  • [ ] Reuse existing enum schemas
  • [ ] Add refinements if needed
  • [ ] Write unit tests
  • [ ] Document with JSDoc
  • [ ] Build library: pnpm nx build shared-validation

Library Structure

libs/shared/
├── types/                   # @quantum/shared-types
│   └── src/
│       ├── lib/
│       │   ├── user.types.ts       # User models (DB, App, Auth)
│       │   ├── scan.types.ts       # Scan models
│       │   ├── tier.types.ts       # Tier models
│       │   ├── skin-analysis.types.ts  # Analysis results
│       │   ├── treatment.types.ts  # Treatment cycles
│       │   ├── streak.types.ts     # Daily streaks
│       │   └── quota.types.ts      # Quota tracking
│       └── index.ts                # Public API exports
│
├── validation/              # @quantum/shared-validation
│   └── src/
│       ├── lib/
│       │   ├── validation.ts       # Zod schemas
│       │   └── streak.ts           # Streak validation logic
│       └── index.ts                # Public API exports
│
└── utils/                   # @quantum/shared-utils
    └── src/
        ├── lib/
        │   ├── constants.ts        # Shared constants
        │   ├── date-utils.ts       # Date handling utilities
        │   └── utils.ts            # General utilities
        └── index.ts                # Public API exports

Core Principles

1. Single Source of Truth

All shared types, validation, and utilities live in libs/shared/*:

// ✅ GOOD: Import from shared library
import { UserApp, ConsentProfile } from '@quantum/shared-types';
import { personalInfoSchema } from '@quantum/shared-validation';

// ❌ BAD: Duplicate type definition
interface UserApp {
  id: string;
  email: string;
  // ...
}

2. DB vs App Type Separation

Always separate database types from application types:

/**
 * Represents the full user object from the 'users' table
 */
export interface UserDB {
  id: string;
  email: string;
  full_name: string;
  clerk_user_id: string;
  tier_id: string;  // Foreign key
  email_verified: boolean;
  consent_id: string | null;
  created_at: Date;
  updated_at: Date;
  deleted_at: Date | null;
}

/**
 * Safe, application-level representation
 * Omits sensitive fields and includes joined data
 */
export type UserApp = Omit<UserDB, 'tier_id'> & {
  tier: 'Standard' | 'Premium' | 'Admin';  // Joined tier name
  analysis_quota: number;
  consent?: UserConsentDB | null;
};

Pattern:

  • DB types (*DB): Match database schema exactly (snake_case fields, UUIDs, timestamps)
  • App types: Camel case, joined relations, omit sensitive data

3. Type-First, Schema-Second

Define TypeScript types first, then create matching Zod schemas:

// 1. Define type in shared-types
export interface PersonalInfoUpdate {
  age: number;
  sex: SurveySex;
  skinType: string;
  ethnicity: string;
  country: string;
}

// 2. Create matching Zod schema in shared-validation
export const personalInfoSchema = z.object({
  age: z.number().int().min(13).max(120),
  sex: surveySexSchema,
  skinType: z.string(),
  ethnicity: z.string(),
  country: countryCodeSchema,
});

// Type should match schema inference
type PersonalInfoUpdate = z.infer<typeof personalInfoSchema>;

4. Reusable Enum Schemas

Define enums once, reuse everywhere:

// validation.ts
const SURVEY_SEX_VALUES = ['male', 'female', 'non_binary', 'prefer_not_to_say'] as const;
export const surveySexSchema = z.enum(SURVEY_SEX_VALUES, {
  message: 'Please select a valid option',
});
export type SurveySex = z.infer<typeof surveySexSchema>;

// Reuse in multiple schemas
export const surveySchema = z.object({
  sex: surveySexSchema,  // ✅ Reused
  // ...
});

export const personalInfoSchema = z.object({
  sex: surveySexSchema,  // ✅ Reused
  // ...
});

5. Custom Error Messages

Always provide clear, user-friendly error messages:

export const emailSchema = z
  .string({
    required_error: 'Email is required',
    invalid_type_error: 'Email must be a string',
  })
  .email('Please enter a valid email address')
  .min(3, 'Email must be at least 3 characters')
  .max(255, 'Email must be less than 255 characters');

export const ageSchema = z
  .number({
    required_error: 'Age is required',
    invalid_type_error: 'Age must be a number',
  })
  .int('Age must be a whole number')
  .min(13, 'You must be at least 13 years old')
  .max(120, 'Please enter a valid age');

Type Patterns

User Types

// Core user types with documentation
export interface UserDB {
  id: string;
  email: string;
  full_name: string;
  clerk_user_id: string;
  tier_id: string;
  email_verified: boolean;
  consent_id: string | null;
  created_at: Date;
  updated_at: Date;
  deleted_at: Date | null;
}

export type UserApp = Omit<UserDB, 'tier_id'> & {
  tier: 'Standard' | 'Premium' | 'Admin';
  analysis_quota: number;
  consent?: UserConsentDB | null;
};

export type AuthUser = UserApp;  // Semantic alias

Enum Types

export type ConsentProfile = 'GDPR' | 'BIPA' | 'CCPA' | 'ROW';
export type TierName = 'Standard' | 'Premium' | 'Admin';
export type TreatmentGroup = 'GROUP_A' | 'GROUP_B' | 'GROUP_C' | 'UNGROUPED';

API Response Types

export interface QuotaStatus {
  currentPeriod: {
    used: number;
    limit: number;
    remaining: number;
  };
  lifetime: {
    total: number;
  };
  tier: TierName;
}

export interface AnalysisResult {
  success: boolean;
  data?: SkinAnalysisData;
  error?: string;
}

Validation Patterns

Basic Schemas

export const emailSchema = z
  .string()
  .email('Please enter a valid email address')
  .min(3)
  .max(255);

export const passwordSchema = z
  .string()
  .min(8, 'Password must be at least 8 characters')
  .max(100, 'Password must be less than 100 characters');

Object Schemas

export const personalInfoSchema = z.object({
  age: z.number().int().min(13).max(120),
  sex: surveySexSchema,
  skinType: z.string(),
  ethnicity: z.string(),
  country: countryCodeSchema,
  skinConcerns: z.array(skinConcernSchema).optional(),
});

export type PersonalInfoData = z.infer<typeof personalInfoSchema>;

Refinements (Complex Validation)

export const changePasswordSchema = z
  .object({
    currentPassword: z.string().min(1, 'Current password is required'),
    newPassword: passwordSchema,
    confirmPassword: z.string().min(1, 'Please confirm your password'),
  })
  .refine((data) => data.newPassword === data.confirmPassword, {
    message: 'Passwords do not match',
    path: ['confirmPassword'],
  });

Transformations

export const countryCodeSchema = z
  .string()
  .length(2, 'Country code must be 2 characters')
  .transform((c) => c.toUpperCase());  // Auto-uppercase

export const emailNormalizedSchema = z
  .string()
  .email()
  .transform((e) => e.toLowerCase().trim());

Optional Fields

export const updateUserSchema = z.object({
  fullName: z.string().min(1).optional(),
  age: z.number().int().min(13).max(120).optional(),
  skinConcerns: z.array(skinConcernSchema).optional(),
});

Utility Patterns

Date Utilities

// libs/shared/utils/src/lib/date-utils.ts

/**
 * Get today's date in YYYY-MM-DD format
 */
export function getTodayYmd(): string {
  return new Date().toISOString().split('T')[0];
}

/**
 * Parse YYYY-MM-DD string to Date object
 */
export function parseYmd(ymd: string | undefined): Date | null {
  if (!ymd) return null;
  const [y, m, d] = ymd.split('-').map((s) => Number(s));
  if (!y || !m || !d) return null;
  const date = new Date(Date.UTC(y, m - 1, d));
  return isNaN(date.getTime()) ? null : date;
}

/**
 * Convert Date object to YYYY-MM-DD string
 */
export function toYmd(date: Date): string {
  const y = date.getUTCFullYear();
  const m = String(date.getUTCMonth() + 1).padStart(2, '0');
  const d = String(date.getUTCDate()).padStart(2, '0');
  return `${y}-${m}-${d}`;
}

Constants

// libs/shared/utils/src/lib/constants.ts

export const API_VERSION = 'v1';
export const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
export const SUPPORTED_IMAGE_TYPES = ['image/jpeg', 'image/png'];

Common Workflows

Adding a New Type

  1. Create type in appropriate file:

    // libs/shared/types/src/lib/feature.types.ts
    export interface FeatureDB {
      id: string;
      user_id: string;
      data: string;
      created_at: Date;
    }
    
    export interface FeatureApp {
      id: string;
      userId: string;
      data: string;
      createdAt: Date;
    }
    
  2. Export from index:

    // libs/shared/types/src/index.ts
    export * from './lib/feature.types.js';
    
  3. Build library:

    pnpm nx build shared-types
    

Adding a New Validation Schema

  1. Define schema:

    // libs/shared/validation/src/lib/validation.ts
    export const featureSchema = z.object({
      data: z.string().min(1).max(1000),
    });
    
    export type FeatureData = z.infer<typeof featureSchema>;
    
  2. Export from index:

    // libs/shared/validation/src/index.ts
    export { featureSchema } from './lib/validation.js';
    export type { FeatureData } from './lib/validation.js';
    
  3. Write tests:

    // libs/shared/validation/src/lib/validation.spec.ts
    describe('featureSchema', () => {
      it('should validate valid data', () => {
        const result = featureSchema.safeParse({ data: 'test' });
        expect(result.success).toBe(true);
      });
    });
    
  4. Build library:

    pnpm nx build shared-validation
    

Using Shared Libraries

// Backend controller
import { UserApp, QuotaStatus } from '@quantum/shared-types';
import { personalInfoSchema } from '@quantum/shared-validation';

export async function updatePersonalInfo(req: Request, res: Response) {
  const result = personalInfoSchema.safeParse(req.body);
  if (!result.success) {
    return res.status(400).json({ errors: formatZodErrors(result.error) });
  }
  // ...
}

// Frontend component
import type { UserApp } from '@quantum/shared-types';
import { personalInfoSchema } from '@quantum/shared-validation';

export function ProfileForm() {
  const handleSubmit = async (data: unknown) => {
    const result = personalInfoSchema.safeParse(data);
    if (!result.success) {
      setErrors(result.error.flatten().fieldErrors);
      return;
    }
    await submitProfile(result.data);
  };
}

Anti-Patterns

❌ Don't Duplicate Types

// BAD: Duplicating type in backend
interface UserApp {
  id: string;
  email: string;
  // ...
}

// GOOD: Import from shared
import { UserApp } from '@quantum/shared-types';

❌ Don't Skip Type/Schema Sync

// BAD: Type and schema out of sync
export interface PersonalInfo {
  age: number;
  country: string;  // Type has country
}

export const personalInfoSchema = z.object({
  age: z.number(),
  // Missing country in schema!
});

// GOOD: Keep in sync
export interface PersonalInfo {
  age: number;
  country: string;
}

export const personalInfoSchema = z.object({
  age: z.number(),
  country: countryCodeSchema,
});

❌ Don't Use DB Types in API Responses

// BAD: Exposing DB type
export function getUser(): UserDB {
  // Exposes internal IDs, timestamps
}

// GOOD: Use App type
export function getUser(): UserApp {
  // Safe for API responses
}

Testing

Type Tests

// libs/shared/types/src/lib/__tests__/scan.types.spec.ts
import type { ScanDB, ScanApp } from '../scan.types';

describe('Scan types', () => {
  it('should have ScanDB with DB fields', () => {
    const scan: ScanDB = {
      id: '123',
      user_id: '456',
      created_at: new Date(),
      // ...
    };
    expect(scan).toBeDefined();
  });
});

Validation Tests

// libs/shared/validation/src/lib/validation.spec.ts
import { personalInfoSchema } from './validation';

describe('personalInfoSchema', () => {
  it('should validate valid data', () => {
    const result = personalInfoSchema.safeParse({
      age: 25,
      sex: 'male',
      skinType: 'normal',
      ethnicity: 'asian',
      country: 'US',
    });
    expect(result.success).toBe(true);
  });

  it('should reject invalid age', () => {
    const result = personalInfoSchema.safeParse({
      age: 12, // Too young
      sex: 'male',
      skinType: 'normal',
      ethnicity: 'asian',
      country: 'US',
    });
    expect(result.success).toBe(false);
  });
});

Building & Publishing

Build Single Library

pnpm nx build shared-types
pnpm nx build shared-validation
pnpm nx build shared-utils

Build All Shared Libraries

pnpm nx run-many -t build -p shared-types shared-validation shared-utils

Watch Mode (Development)

pnpm nx watch shared-types -- pnpm nx build shared-types

Reference


Related Skills

  • backend-dev-guidelines - Backend usage of shared types
  • frontend-dev-guidelines - Frontend usage of shared types
  • data-access-guidelines - DB type mappings

Skill Status: Created for Quantum Skincare ✅ Stack: TypeScript, Zod, Nx Monorepo Libraries: @quantum/shared-types, @quantum/shared-validation, @quantum/shared-utils Line Count: Under 500 lines (following Anthropic best practices) ✅