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#
- Make type changes in
src/types/src/api/ - Shared types auto-rebuild (if using
npm run dev:types) - Both frontend and backend get updated types automatically
- 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.jsonwith workspaces - Create
src/types/package.json - Create
src/types/tsconfig.json - Update
src/webapp/package.jsondependencies - Run
npm installto 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#
- Always run shared types in watch mode during development
- Make type changes first before implementing features
- Use TypeScript strict mode to catch issues early
- 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#
-
"Cannot find module '@annos/types'"
- Run
npm installin root to setup workspace links - Ensure shared types are built:
npm run build:types
- Run
-
Type mismatches between frontend and backend
- Check that both are using the same version of shared types
- Rebuild shared types:
npm run build:types
-
Hot reload not working for type changes
- Ensure
npm run dev:typesis running in watch mode - Restart development servers if needed
- Ensure
Debugging Tips#
- Use
npm ls @annos/typesto check workspace linking - Check
src/types/dist/for compiled output - 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