# Server TypeScript Conversion Plan ## Overview Convert the Node.js server from JavaScript to TypeScript to improve type safety, developer experience, and enable proper imports of TypeScript modules (database, schemas, etc.) from within the server code. ## Current State Analysis ### Files to Convert 1. **`server.js`** (57 lines) - Main server entry point - Handles Vite dev server setup - Production build serving - Socket.IO initialization - Express middleware 2. **`server/socket.js`** (89 lines) - Socket.IO server - Connection handling - Draft room management - Global instance storage ### Existing TypeScript Configuration - **`tsconfig.node.json`** already includes `server/**/*.ts` - Module resolution is set to "bundler" with ES2022 target - Path aliases configured: `~/` for app, `~/database/` for database ### Dependencies Already Installed - TypeScript is already in devDependencies - `@types/node` is configured in tsconfig.node.json - Vite handles TypeScript compilation in development ## Implementation Steps ### Step 1: Install Required Dependencies ```bash npm install --save-dev tsx @types/express @types/morgan @types/compression ``` **Why tsx?** - Fastest TypeScript runner for Node.js - No compilation step needed - Works in both development and production - Supports ESM modules and path aliases - Better than ts-node for performance ### Step 2: Convert server.js to server.ts **File: `server.ts`** ```typescript import compression from "compression"; import express, { Express } from "express"; import morgan from "morgan"; import { createServer } from "http"; import { ViteDevServer } from "vite"; import path from "path"; import { fileURLToPath } from "url"; // ESM module resolution helpers const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // Configuration const BUILD_PATH = "./build/server/index.js"; const DEVELOPMENT = process.env.NODE_ENV === "development"; const PORT = Number.parseInt(process.env.PORT || "3000", 10); async function createAppServer(): Promise { const app: Express = express(); app.use(compression()); app.disable("x-powered-by"); if (DEVELOPMENT) { console.log("Starting development server"); // Dynamic import to avoid bundling Vite in production const { createServer: createViteServer } = await import("vite"); const viteDevServer: ViteDevServer = await createViteServer({ server: { middlewareMode: true }, }); app.use(viteDevServer.middlewares); app.use(async (req, res, next) => { try { const source = await viteDevServer.ssrLoadModule("./server/app.ts"); return await source.app(req, res, next); } catch (error) { if (error instanceof Error) { viteDevServer.ssrFixStacktrace(error); } next(error); } }); } else { console.log("Starting production server"); app.use( "/assets", express.static("build/client/assets", { immutable: true, maxAge: "1y" }) ); app.use(morgan("tiny")); app.use(express.static("build/client", { maxAge: "1h" })); // Dynamic import for production build const { app: productionApp } = await import(BUILD_PATH); app.use(productionApp); } // Create HTTP server const httpServer = createServer(app); // Initialize Socket.IO const { initializeSocketIO } = await import("./server/socket.js"); initializeSocketIO(httpServer); // Start server httpServer.listen(PORT, () => { console.log(`Server is running on http://localhost:${PORT}`); }); } // Start the server createAppServer().catch((error) => { console.error("Failed to start server:", error); process.exit(1); }); ``` ### Step 3: Convert server/socket.js to server/socket.ts **File: `server/socket.ts`** ```typescript import { Server as SocketIOServer, Socket } from "socket.io"; import { Server as HTTPServer } from "http"; // Global type augmentation for Socket.IO instance declare global { var __socketIO: SocketIOServer | undefined; } let io: SocketIOServer | null = null; /** * Initialize Socket.IO server */ export function initializeSocketIO(httpServer: HTTPServer): SocketIOServer { if (io) { console.log("Socket.IO already initialized"); return io; } // Create Socket.IO server with proper typing io = new SocketIOServer(httpServer, { cors: process.env.NODE_ENV === "production" && process.env.APP_URL ? { origin: process.env.APP_URL, credentials: true, } : undefined, }); // Connection handling with typed socket io.on("connection", (socket: Socket) => { console.log("Client connected:", socket.id); // Join draft room socket.on("join-draft", (seasonId: string) => { if (!seasonId) { console.error("No seasonId provided for join-draft"); return; } socket.join(`draft-${seasonId}`); console.log(`Socket ${socket.id} joined draft-${seasonId}`); }); // Leave draft room socket.on("leave-draft", (seasonId: string) => { if (!seasonId) return; socket.leave(`draft-${seasonId}`); console.log(`Socket ${socket.id} left draft-${seasonId}`); }); // Test event handler socket.on("test-event", (data: any) => { console.log("📨 Received test-event from client:", socket.id, data); socket.emit("test-message", { originalMessage: data, serverResponse: "Hello from server!", serverTimestamp: new Date().toISOString(), socketId: socket.id, }); console.log("✅ Sent test-message response to client:", socket.id); }); socket.on("disconnect", () => { console.log("Client disconnected:", socket.id); }); }); // Store globally for access in route handlers global.__socketIO = io; console.log("Socket.IO initialized"); return io; } /** * Get the Socket.IO server instance */ export function getSocketIO(): SocketIOServer { const instance = io || global.__socketIO; if (!instance) { throw new Error("Socket.IO not initialized. Call initializeSocketIO first."); } return instance; } ``` ### Step 4: Create Type Definitions for Socket Events (Optional but Recommended) **File: `server/socket.types.ts`** ```typescript // Define your socket event types for better type safety export interface ServerToClientEvents { "test-message": (data: { originalMessage: any; serverResponse: string; serverTimestamp: string; socketId: string; }) => void; "pick-made": (data: any) => void; "draft-started": (data: { seasonId: string; currentPickNumber: number }) => void; "draft-completed": () => void; "timer-update": (data: { seasonId: string; teamId: string; timeRemaining: number; currentPickNumber: number; }) => void; } export interface ClientToServerEvents { "join-draft": (seasonId: string) => void; "leave-draft": (seasonId: string) => void; "test-event": (data: any) => void; } export interface InterServerEvents { ping: () => void; } export interface SocketData { userId?: string; teamId?: string; } ``` Then update `server/socket.ts` to use these types: ```typescript import { Server as SocketIOServer } from "socket.io"; import type { ServerToClientEvents, ClientToServerEvents, InterServerEvents, SocketData } from "./socket.types"; // Create typed Socket.IO server io = new SocketIOServer< ClientToServerEvents, ServerToClientEvents, InterServerEvents, SocketData >(httpServer, { /* ... */ }); ``` ### Step 5: Update package.json Scripts ```json { "scripts": { "build": "react-router build", "db:generate": "dotenv -- drizzle-kit generate", "db:migrate": "dotenv -- drizzle-kit migrate", "dev": "dotenv -- tsx watch server.ts", "start": "tsx server.ts", "start:production": "drizzle-kit migrate && tsx server.ts", "typecheck": "react-router typegen && tsc -b" } } ``` **Key Changes:** - `dev`: Uses `tsx watch` for auto-restart on file changes - `start`: Uses `tsx` to run TypeScript directly - `start:production`: Same as start but with migration ### Step 6: Update Dockerfile ```dockerfile FROM node:20-alpine AS development-dependencies-env COPY . /app WORKDIR /app RUN npm ci FROM node:20-alpine AS production-dependencies-env COPY ./package.json package-lock.json /app/ WORKDIR /app RUN npm ci --omit=dev FROM node:20-alpine AS build-env COPY . /app/ COPY --from=development-dependencies-env /app/node_modules /app/node_modules WORKDIR /app RUN npm run build FROM node:20-alpine # Copy TypeScript files instead of JavaScript COPY ./package.json package-lock.json server.ts /app/ COPY --from=production-dependencies-env /app/node_modules /app/node_modules COPY --from=build-env /app/build /app/build COPY ./drizzle /app/drizzle COPY ./drizzle.config.ts /app/drizzle.config.ts COPY ./server /app/server COPY ./database /app/database COPY ./app /app/app # Copy TypeScript configs for tsx runtime COPY ./tsconfig*.json /app/ WORKDIR /app # Install tsx in production for running TypeScript RUN npm install tsx CMD ["npm", "run", "start:production"] ``` ### Step 7: Update server.ts Import Path After converting socket.js to socket.ts, update the import in server.ts: ```typescript // Change from: const { initializeSocketIO } = await import("./server/socket.js"); // To: const { initializeSocketIO } = await import("./server/socket"); // Or explicitly: const { initializeSocketIO } = await import("./server/socket.ts"); ``` ### Step 8: Add Timer Functionality (Now Possible!) With TypeScript server, you can now properly import database modules: **File: `server/timer.ts`** ```typescript import { database } from "~/database/context"; import * as schema from "~/database/schema"; import { eq, and } from "drizzle-orm"; import { getSocketIO } from "./socket"; export async function startDraftTimerSystem(): Promise { setInterval(async () => { try { await updateDraftTimers(); } catch (error) { console.error("[Timer] Error updating draft timers:", error); } }, 1000); console.log("[Timer] Draft timer system started"); } async function updateDraftTimers(): Promise { const db = database(); const io = getSocketIO(); // Get all active drafts const activeDrafts = await db.query.seasons.findMany({ where: eq(schema.seasons.status, "draft"), }); // ... rest of timer logic } ``` Then in `server/socket.ts`, add: ```typescript import { startDraftTimerSystem } from "./timer"; export function initializeSocketIO(httpServer: HTTPServer): SocketIOServer { // ... existing code ... // Start timer system after Socket.IO is initialized startDraftTimerSystem(); return io; } ``` ## Testing Plan ### 1. Development Mode Testing ```bash # Start dev server with TypeScript npm run dev # Verify: - Server starts without errors - Vite dev server works - Socket.IO connections work - Hot reload works with tsx watch - Can import database modules ``` ### 2. Production Build Testing ```bash # Build the app npm run build # Start production server NODE_ENV=production npm start # Verify: - Server starts without errors - Static assets are served - Socket.IO works in production - No TypeScript compilation errors ``` ### 3. Docker Testing ```bash # Build Docker image docker build -t brackt-test . # Run container docker run -p 3000:3000 brackt-test # Verify: - Container starts successfully - Application works in containerized environment ``` ## Benefits of This Approach 1. **Type Safety**: Full TypeScript throughout the server 2. **Better Imports**: Can import TypeScript modules directly 3. **No Build Step**: tsx runs TypeScript directly 4. **Fast Development**: tsx watch provides fast restarts 5. **Production Ready**: tsx works in production without compilation 6. **Maintainable**: Cleaner code with proper types 7. **Debugging**: Better stack traces and error messages ## Migration Checklist - [ ] Install tsx and type packages - [ ] Convert server.js to server.ts - [ ] Convert server/socket.js to server/socket.ts - [ ] Create socket.types.ts for event types - [ ] Update package.json scripts - [ ] Update Dockerfile - [ ] Test development mode - [ ] Test production build - [ ] Test Docker build - [ ] Implement timer system with database imports - [ ] Remove old .js files ## Rollback Plan If issues arise: 1. Keep original .js files until testing complete 2. Can revert package.json scripts to use .js files 3. Git commit before starting conversion 4. Test thoroughly in development before deploying ## Notes for Implementation - tsx handles TypeScript path aliases (`~/`) automatically - No need for separate compilation step - Works with existing Vite setup - Compatible with React Router v7 SSR - Maintains existing Socket.IO functionality - Enables proper database module imports