A social knowledge tool for researchers built on ATProto

Shared Types Architecture - npm Workspaces Implementation#

This document outlines the implementation plan for sharing types between the backend (src/) and webapp (src/webapp/) using npm workspaces in our monorepo structure.

Overview#

We use npm workspaces to create a shared types package that both backend and frontend import from as a proper npm dependency, ensuring type safety and consistency across the entire application.

Architecture Decision: npm Workspaces#

We chose npm workspaces over simpler approaches because:

  • Industry standard - Professional monorepo structure
  • Proper dependency management - npm handles versioning and dependencies
  • Scalability - Easy to add more packages (mobile app, CLI tools, etc.)
  • Build isolation - Each package has its own build process
  • Publishing ready - Can publish shared types as separate npm package
  • IDE support - Better IntelliSense and go-to-definition
  • Version management - Can version shared types independently

Final Directory Structure#

annos/
├── package.json                    # Workspace root
├── src/
│   ├── types/                     # @annos/types package
│   │   ├── package.json
│   │   ├── tsconfig.json
│   │   ├── src/
│   │   │   ├── api/
│   │   │   │   ├── index.ts       # Re-exports all types
│   │   │   │   ├── common.ts      # Common types (User, Pagination, etc.)
│   │   │   │   ├── requests.ts    # Request types for all API endpoints
│   │   │   │   └── responses.ts   # Response types for all API endpoints
│   │   │   └── index.ts           # Main entry point
│   │   └── dist/                  # Compiled output
│   ├── shared/                    # EXISTING: Backend shared utilities
│   ├── modules/                   # Backend modules
│   └── webapp/
│       ├── package.json           # @annos/webapp package
│       └── ...

Implementation Plan#

Phase 1: Setup Workspace Infrastructure#

Step 1.1: Configure Root Workspace#

Update root package.json:

{
  "name": "annos",
  "version": "1.0.0",
  "workspaces": ["src/types", "src/webapp", "."],
  "scripts": {
    "build:types": "npm run build --workspace=@annos/types",
    "dev:types": "npm run dev --workspace=@annos/types",
    "build:webapp": "npm run build --workspace=@annos/webapp",
    "dev:webapp": "npm run dev --workspace=@annos/webapp",
    "dev:all": "npm run dev:types & npm run dev:webapp & npm run dev:app:inner"
  }
}

Step 1.2: Create Shared Types Package#

Create src/types/package.json:

{
  "name": "@annos/types",
  "version": "1.0.0",
  "description": "Shared TypeScript types for Annos API",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "files": ["dist/**/*"],
  "scripts": {
    "build": "tsc",
    "dev": "tsc --watch",
    "clean": "rm -rf dist"
  },
  "devDependencies": {
    "typescript": "^5.8.3"
  }
}

Create src/types/tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "CommonJS",
    "lib": ["ES2020"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

Step 1.3: Update Webapp Package#

Update src/webapp/package.json:

{
  "name": "@annos/webapp",
  "dependencies": {
    "@annos/types": "workspace:*"
    // ... existing dependencies
  }
}

Phase 2: Migrate Types to Shared Package#

Step 2.1: Create Shared Type Files#

Move and organize existing webapp types into the shared package:

src/types/src/api/common.ts:

export interface User {
  id: string;
  name: string;
  handle: string;
  avatarUrl?: string;
  description?: string;
}

export interface Pagination {
  currentPage: number;
  totalPages: number;
  totalCount: number;
  hasMore: boolean;
  limit: number;
}

export interface BaseSorting {
  sortOrder: 'asc' | 'desc';
}

export interface CardSorting extends BaseSorting {
  sortBy: 'createdAt' | 'updatedAt' | 'libraryCount';
}

export interface CollectionSorting extends BaseSorting {
  sortBy: 'name' | 'createdAt' | 'updatedAt' | 'cardCount';
}

export interface FeedPagination extends Pagination {
  nextCursor?: string;
}

src/types/src/api/requests.ts:

// Copy all request types from src/webapp/api-client/types/requests.ts
export interface PaginationParams {
  page?: number;
  limit?: number;
}

export interface SortingParams {
  sortBy?: string;
  sortOrder?: 'asc' | 'desc';
}

// ... all other request types

src/types/src/api/responses.ts:

import {
  User,
  Pagination,
  CardSorting,
  CollectionSorting,
  FeedPagination,
} from './common';

// Copy all response types from src/webapp/api-client/types/responses.ts
export interface UrlCard {
  id: string;
  type: 'URL';
  url: string;
  // ... rest of UrlCard interface
}

// ... all other response types

src/types/src/api/index.ts:

export * from './common';
export * from './requests';
export * from './responses';

src/types/src/index.ts:

export * from './api';

Step 2.2: Build Shared Types#

cd src/types
npm run build

Phase 3: Update Frontend to Use Shared Types#

Step 3.1: Install Shared Types Dependency#

npm install --workspace=@annos/webapp

Step 3.2: Update Frontend Imports#

Replace all imports in webapp files:

// OLD: src/webapp/api-client/ApiClient.ts
import type {
  GetUrlCardsResponse,
  AddUrlToLibraryRequest,
} from './types/responses';

// NEW:
import type { GetUrlCardsResponse, AddUrlToLibraryRequest } from '@annos/types';

Step 3.3: Remove Old Type Files#

rm -rf src/webapp/api-client/types/

Phase 4: Update Backend to Use Shared Types#

Step 4.1: Install Shared Types in Backend#

Add to root package.json dependencies:

{
  "dependencies": {
    "@annos/types": "workspace:*"
  }
}

Step 4.2: Update Use Cases#

// src/modules/cards/application/useCases/queries/GetUrlCardsUseCase.ts
import { GetUrlCardsResponse } from '@annos/types';

export class GetUrlCardsUseCase {
  async execute(
    query: GetUrlCardsQuery,
  ): Promise<
    Result<GetUrlCardsResponse, ValidationError | AppError.UnexpectedError>
  > {
    // Implementation must return GetUrlCardsResponse type
    return ok({
      cards: enrichedCards,
      pagination: {
        currentPage: page,
        totalPages: Math.ceil(result.totalCount / limit),
        totalCount: result.totalCount,
        hasMore: page * limit < result.totalCount,
        limit,
      },
      sorting: {
        sortBy,
        sortOrder,
      },
    });
  }
}

Step 4.3: Update Controllers#

// src/modules/cards/infrastructure/http/controllers/GetMyUrlCardsController.ts
import { GetUrlCardsResponse } from '@annos/types';

export class GetMyUrlCardsController extends Controller {
  async executeImpl(req: AuthenticatedRequest, res: Response): Promise<any> {
    const result = await this.getUrlCardsUseCase.execute(query);

    if (result.isErr()) {
      return this.fail(res, result.error);
    }

    // result.value is GetUrlCardsResponse type - guaranteed by TypeScript
    return this.ok(res, result.value);
  }
}

Phase 5: Development Workflow#

Step 5.1: Development Scripts#

Add to root package.json:

{
  "scripts": {
    "dev": "concurrently \"npm run dev:types\" \"npm run dev:webapp\" \"npm run dev:app:inner\"",
    "dev:types": "npm run dev --workspace=@annos/types",
    "build:all": "npm run build:types && npm run build:webapp && npm run build"
  }
}

Step 5.2: Type Development Workflow#

  1. Make type changes in src/types/src/api/
  2. Shared types auto-rebuild (if using npm run dev:types)
  3. Both frontend and backend get updated types automatically
  4. TypeScript compiler catches any mismatches immediately

Phase 6: Testing and Validation#

Step 6.1: Type Safety Validation#

# Check all TypeScript compilation
npm run type-check
npm run type-check --workspace=@annos/webapp
npm run build:types

Step 6.2: Runtime Validation (Optional)#

Add Zod schemas for runtime validation:

src/types/src/validation/index.ts:

import { z } from 'zod';

export const UrlCardSchema = z.object({
  id: z.string(),
  type: z.literal('URL'),
  url: z.string().url(),
  // ... other fields
});

export const GetUrlCardsResponseSchema = z.object({
  cards: z.array(UrlCardSchema),
  pagination: z.object({
    currentPage: z.number(),
    totalPages: z.number(),
    totalCount: z.number(),
    hasMore: z.boolean(),
    limit: z.number(),
  }),
  sorting: z.object({
    sortBy: z.enum(['createdAt', 'updatedAt', 'libraryCount']),
    sortOrder: z.enum(['asc', 'desc']),
  }),
});

Migration Checklist#

Phase 1: Infrastructure ✅#

  • Update root package.json with workspaces
  • Create src/types/package.json
  • Create src/types/tsconfig.json
  • Update src/webapp/package.json dependencies
  • Run npm install to setup workspace

Phase 2: Type Migration ✅#

  • Create src/types/src/api/common.ts
  • Create src/types/src/api/requests.ts
  • Create src/types/src/api/responses.ts
  • Create src/types/src/api/index.ts
  • Create src/types/src/index.ts
  • Build shared types: npm run build:types

Phase 3: Frontend Migration ✅#

  • Update all imports in webapp to use @annos/types
  • Remove old type files: rm -rf src/webapp/api-client/types/
  • Test webapp compilation: npm run type-check --workspace=@annos/webapp

Phase 4: Backend Migration ✅#

  • Add shared types dependency to root package
  • Update use cases to import and return shared types
  • Update controllers to use shared types
  • Test backend compilation: npm run type-check

Phase 5: Development Setup ✅#

  • Add development scripts to root package.json
  • Test concurrent development: npm run dev
  • Verify hot reload works for type changes

Phase 6: Validation ✅#

  • Run full type check across all packages
  • Test API endpoints return correct types
  • Add runtime validation (optional)
  • Update documentation

Best Practices#

Type Naming Conventions#

  • Requests: {Action}{Resource}Request (e.g., GetUrlCardsRequest)
  • Responses: {Action}{Resource}Response (e.g., GetUrlCardsResponse)
  • Common types: Descriptive names (e.g., User, Pagination)

Development Workflow#

  1. Always run shared types in watch mode during development
  2. Make type changes first before implementing features
  3. Use TypeScript strict mode to catch issues early
  4. Version shared types when making breaking changes

Error Handling#

  • Define consistent error response types
  • Use discriminated unions for different error types
  • Include error codes and messages in shared types

Troubleshooting#

Common Issues#

  1. "Cannot find module '@annos/types'"

    • Run npm install in root to setup workspace links
    • Ensure shared types are built: npm run build:types
  2. Type mismatches between frontend and backend

    • Check that both are using the same version of shared types
    • Rebuild shared types: npm run build:types
  3. Hot reload not working for type changes

    • Ensure npm run dev:types is running in watch mode
    • Restart development servers if needed

Debugging Tips#

  1. Use npm ls @annos/types to check workspace linking
  2. Check src/types/dist/ for compiled output
  3. Use IDE "Go to Definition" to verify imports are resolving correctly

Future Enhancements#

  • OpenAPI generation: Generate OpenAPI specs from shared types
  • Runtime validation: Add Zod schemas for all shared types
  • Documentation: Auto-generate API docs from types
  • Testing: Create type-safe test utilities
  • Publishing: Publish shared types to private npm registry
  • Versioning: Implement semantic versioning for breaking changes