# DreamChat Monorepo Guide ## Overview DreamChat uses a pnpm workspace monorepo structure to share code between frontend and backend, particularly for WebSocket types and API DTOs. ## Monorepo Structure ``` dreamchat/ ├── apps/ │ ├── backend/ # NestJS application │ └── frontend/ # React + Vite application ├── packages/ │ ├── shared/ # Shared types & interfaces │ └── config/ # Shared configurations (ESLint, TS) ├── prisma/ # Database schema (shared) ├── pnpm-workspace.yaml └── package.json # Root package.json ``` ## Root Configuration ### package.json ```json { "name": "dreamchat", "version": "1.0.0", "private": true, "packageManager": "pnpm@8.15.0", "scripts": { "build": "pnpm -r build", "dev": "pnpm -r --parallel dev", "test": "pnpm -r test", "lint": "pnpm -r lint", "db:migrate": "pnpm --filter backend db:migrate", "db:generate": "prisma generate", "db:studio": "prisma studio", "clean": "pnpm -r clean && rm -rf node_modules" }, "devDependencies": { "@types/node": "^20.0.0", "typescript": "^5.3.0" } } ``` ### pnpm-workspace.yaml ```yaml packages: - 'apps/*' - 'packages/*' ``` ### .npmrc ``` shamefully-hoist=true auto-install-peers=true strict-peer-dependencies=false ``` ## Package Structure ### 1. Shared Package (`packages/shared`) Contains all shared types, interfaces, and DTOs used by both frontend and backend. #### packages/shared/package.json ```json { "name": "@dreamchat/shared", "version": "1.0.0", "main": "./dist/index.js", "types": "./dist/index.d.ts", "exports": { ".": { "import": "./dist/index.js", "require": "./dist/index.js", "types": "./dist/index.d.ts" }, "./websocket": { "import": "./dist/websocket/index.js", "types": "./dist/websocket/index.d.ts" }, "./api": { "import": "./dist/api/index.js", "types": "./dist/api/index.d.ts" } }, "scripts": { "build": "tsc", "dev": "tsc --watch", "clean": "rm -rf dist" }, "devDependencies": { "typescript": "^5.3.0" } } ``` #### packages/shared/tsconfig.json ```json { "compilerOptions": { "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", "declaration": true, "outDir": "./dist", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] } ``` #### packages/shared/src/websocket/events.ts ```typescript /** * WebSocket Event Types * Shared between frontend and backend for type-safe communication */ export enum WebSocketEventType { // Client -> Server JOIN_CONVERSATION = 'JOIN_CONVERSATION', LEAVE_CONVERSATION = 'LEAVE_CONVERSATION', SEND_MESSAGE = 'SEND_MESSAGE', STOP_GENERATION = 'STOP_GENERATION', // Server -> Client CONVERSATION_JOINED = 'CONVERSATION_JOINED', MESSAGE_ACK = 'MESSAGE_ACK', STREAM_CHUNK = 'STREAM_CHUNK', STREAM_COMPLETE = 'STREAM_COMPLETE', ERROR = 'ERROR', } // Base message interface export interface WebSocketMessage { type: WebSocketEventType; payload: T; timestamp: string; requestId?: string; } // Client -> Server payloads export interface JoinConversationPayload { conversationId: string; } export interface LeaveConversationPayload { conversationId: string; } export interface SendMessagePayload { conversationId: string; content: string; streaming?: boolean; } export interface StopGenerationPayload { conversationId: string; } // Server -> Client payloads export interface ConversationJoinedPayload { conversationId: string; history: MessageHistoryItem[]; } export interface MessageHistoryItem { id: string; role: 'user' | 'assistant' | 'system'; content: string; createdAt: string; } export interface MessageAckPayload { messageId: string; status: 'received' | 'processing' | 'error'; } export interface StreamChunkPayload { conversationId: string; chunk: string; isComplete: boolean; } export interface StreamCompletePayload { conversationId: string; messageId: string; content: string; tokensUsed?: number; model?: string; } export interface ErrorPayload { code: string; message: string; details?: Record; } ``` #### packages/shared/src/websocket/index.ts ```typescript export * from './events.js'; ``` #### packages/shared/src/api/dto.ts ```typescript /** * Shared API DTOs * Used by backend for validation and frontend for type safety */ // Character DTOs export interface CreateCharacterDto { name: string; personalityPrompt: string; backstory?: string; attributes?: Record; avatarUrl?: string; config?: Record; } export interface UpdateCharacterDto extends Partial {} export interface CharacterResponseDto { id: string; name: string; avatarUrl?: string; personalityPrompt: string; backstory?: string; attributes: Record; config: Record; isPublic: boolean; createdAt: string; updatedAt: string; } // Conversation DTOs export interface CreateConversationDto { characterId: string; title?: string; } export interface ConversationResponseDto { id: string; title?: string; characterId: string; messageCount: number; totalTokens: number; createdAt: string; updatedAt: string; } // Message DTOs export interface CreateMessageDto { content: string; } export interface MessageResponseDto { id: string; role: 'user' | 'assistant' | 'system'; content: string; tokensUsed?: number; model?: string; metadata?: Record; createdAt: string; } // Auth DTOs export interface LoginDto { username: string; password: string; } export interface RegisterDto { email: string; username: string; password: string; } export interface AuthResponseDto { accessToken: string; refreshToken: string; expiresIn: number; user: UserResponseDto; } export interface UserResponseDto { id: string; email: string; username: string; role: 'USER' | 'ADMIN'; } ``` #### packages/shared/src/api/index.ts ```typescript export * from './dto.js'; ``` #### packages/shared/src/index.ts ```typescript export * from './websocket/index.js'; export * from './api/index.js'; ``` ### 2. Backend (`apps/backend`) #### apps/backend/package.json ```json { "name": "@dreamchat/backend", "version": "1.0.0", "scripts": { "build": "nest build", "dev": "nest start --watch", "start": "node dist/main", "test": "jest", "test:watch": "jest --watch", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\"", "db:migrate": "prisma migrate deploy", "db:generate": "prisma generate", "clean": "rm -rf dist" }, "dependencies": { "@dreamchat/shared": "workspace:*", "@nestjs/common": "^10.0.0", "@nestjs/core": "^10.0.0", "@nestjs/platform-socket.io": "^10.0.0", "@nestjs/websockets": "^10.0.0", "@prisma/client": "^5.10.0", "@xenova/transformers": "^2.15.0", "socket.io": "^4.7.0", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.0" }, "devDependencies": { "@nestjs/cli": "^10.0.0", "@nestjs/testing": "^10.0.0", "@types/node": "^20.0.0", "jest": "^29.0.0", "prisma": "^5.10.0", "typescript": "^5.3.0" } } ``` #### Using Shared Package in Backend ```typescript // apps/backend/src/chat/chat.gateway.ts import { WebSocketEventType, WebSocketMessage, JoinConversationPayload, SendMessagePayload, StreamChunkPayload, ConversationJoinedPayload, } from '@dreamchat/shared'; @WebSocketGateway({ namespace: 'chat' }) export class ChatGateway implements OnGatewayConnection { @SubscribeMessage(WebSocketEventType.JOIN_CONVERSATION) async handleJoin( @MessageBody() payload: JoinConversationPayload, @ConnectedSocket() client: Socket, ) { // Implementation const response: WebSocketMessage = { type: WebSocketEventType.CONVERSATION_JOINED, payload: { conversationId: payload.conversationId, history: [] }, timestamp: new Date().toISOString(), }; client.emit(response.type, response); } @SubscribeMessage(WebSocketEventType.SEND_MESSAGE) async handleMessage( @MessageBody() payload: SendMessagePayload, @ConnectedSocket() client: Socket, ) { // Stream chunks with type safety const chunk: WebSocketMessage = { type: WebSocketEventType.STREAM_CHUNK, payload: { conversationId: payload.conversationId, chunk: ' partial text', isComplete: false, }, timestamp: new Date().toISOString(), }; client.emit(chunk.type, chunk); } } ``` ### 3. Frontend (`apps/frontend`) #### apps/frontend/package.json ```json { "name": "@dreamchat/frontend", "version": "1.0.0", "type": "module", "scripts": { "dev": "vite", "build": "tsc && vite build", "preview": "vite preview", "test": "vitest", "lint": "eslint . --ext ts,tsx", "clean": "rm -rf dist" }, "dependencies": { "@dreamchat/shared": "workspace:*", "@tanstack/react-query": "^5.0.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.20.0", "socket.io-client": "^4.7.0", "zustand": "^4.5.0" }, "devDependencies": { "@types/react": "^18.2.0", "@types/react-dom": "^18.2.0", "@vitejs/plugin-react": "^4.2.0", "typescript": "^5.3.0", "vite": "^5.0.0", "vitest": "^1.0.0" } } ``` #### Using Shared Package in Frontend ```typescript // apps/frontend/src/hooks/use-chat.ts import { useCallback } from 'react'; import { io, Socket } from 'socket.io-client'; import { WebSocketEventType, WebSocketMessage, JoinConversationPayload, SendMessagePayload, StreamChunkPayload, StreamCompletePayload, } from '@dreamchat/shared'; export function useChat(conversationId: string) { const [socket, setSocket] = useState(null); const [messages, setMessages] = useState([]); useEffect(() => { const s = io('ws://localhost:3000/chat', { auth: { token: getAuthToken() }, }); s.on(WebSocketEventType.STREAM_CHUNK, (msg: WebSocketMessage) => { setMessages((prev) => [...prev, msg.payload.chunk]); }); s.on(WebSocketEventType.STREAM_COMPLETE, (msg: WebSocketMessage) => { // Handle completion }); setSocket(s); // Join conversation const joinPayload: JoinConversationPayload = { conversationId }; s.emit(WebSocketEventType.JOIN_CONVERSATION, joinPayload); return () => { s.disconnect(); }; }, [conversationId]); const sendMessage = useCallback((content: string) => { if (!socket) return; const payload: SendMessagePayload = { conversationId, content, streaming: true, }; socket.emit(WebSocketEventType.SEND_MESSAGE, payload); }, [socket, conversationId]); return { messages, sendMessage }; } ``` ## Development Workflow ### Initial Setup ```bash # 1. Install pnpm (if not already installed) npm install -g pnpm # 2. Clone and setup git clone cd dreamchat # 3. Install all dependencies pnpm install # 4. Build shared packages first pnpm --filter @dreamchat/shared build # 5. Generate Prisma client pnpm db:generate # 6. Start development (runs all apps in parallel) pnpm dev ``` ### Adding Dependencies ```bash # Add dependency to root pnpm add -D typescript # Add dependency to specific app pnpm --filter @dreamchat/backend add @nestjs/jwt # Add dependency to shared package pnpm --filter @dreamchat/shared add zod # Add shared package to backend/frontend pnpm --filter @dreamchat/backend add @dreamchat/shared@workspace:* ``` ### Building ```bash # Build all packages pnpm build # Build specific package pnpm --filter @dreamchat/shared build # Watch mode for shared package (during development) pnpm --filter @dreamchat/shared dev ``` ### Testing ```bash # Test all pnpm test # Test specific package pnpm --filter @dreamchat/backend test # Test with coverage pnpm --filter @dreamchat/backend test --coverage ``` ## Docker Integration ### Development Docker Compose ```yaml # docker-compose.dev.yml version: '3.8' services: backend: build: context: . dockerfile: apps/backend/Dockerfile target: development volumes: - .:/workspace - /workspace/node_modules - /workspace/apps/backend/node_modules - /workspace/packages/shared/node_modules environment: - NODE_ENV=development command: pnpm --filter @dreamchat/backend dev frontend: build: context: . dockerfile: apps/frontend/Dockerfile target: development volumes: - .:/workspace - /workspace/node_modules - /workspace/apps/frontend/node_modules - /workspace/packages/shared/node_modules command: pnpm --filter @dreamchat/frontend dev ``` ### Production Dockerfile (Backend) ```dockerfile # apps/backend/Dockerfile FROM node:20-alpine AS base RUN npm install -g pnpm FROM base AS dependencies WORKDIR /app COPY pnpm-workspace.yaml package.json ./ COPY apps/backend/package.json ./apps/backend/ COPY packages/shared/package.json ./packages/shared/ RUN pnpm install --frozen-lockfile FROM base AS build WORKDIR /app COPY --from=dependencies /app/node_modules ./node_modules COPY --from=dependencies /app/apps/backend/node_modules ./apps/backend/node_modules COPY --from=dependencies /app/packages/shared/node_modules ./packages/shared/node_modules COPY . . RUN pnpm --filter @dreamchat/shared build RUN pnpm --filter @dreamchat/backend build FROM base AS production WORKDIR /app COPY --from=build /app/apps/backend/dist ./dist COPY --from=build /app/apps/backend/node_modules ./node_modules COPY --from=build /app/node_modules ./node_modules EXPOSE 3000 CMD ["node", "dist/main.js"] ``` ## Benefits of This Structure 1. **Type Safety**: Shared WebSocket events and DTOs ensure frontend and backend stay in sync 2. **Single Source of Truth**: Changes to types in `packages/shared` propagate to both apps 3. **Efficient Development**: pnpm workspaces deduplicate dependencies, saving disk space 4. **Simplified CI/CD**: Single repository with unified versioning 5. **Code Reuse**: Utilities and constants can be shared ## Migration from Non-Monorepo If migrating an existing project: 1. Create new monorepo structure 2. Move backend to `apps/backend` 3. Move frontend to `apps/frontend` 4. Extract shared types to `packages/shared` 5. Update all imports to use `@dreamchat/shared` 6. Update Docker files to use pnpm