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
-
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; } -
Export from index:
// libs/shared/types/src/index.ts export * from './lib/feature.types.js'; -
Build library:
pnpm nx build shared-types
Adding a New Validation Schema
-
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>; -
Export from index:
// libs/shared/validation/src/index.ts export { featureSchema } from './lib/validation.js'; export type { FeatureData } from './lib/validation.js'; -
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); }); }); -
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
- Types: libs/shared/types/src/lib/
- Validation: libs/shared/validation/src/lib/validation.ts
- Utils: libs/shared/utils/src/lib/
- CLAUDE.md: Shared Contracts section
- Zod Docs: https://zod.dev
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) ✅
微信扫一扫