docs
src
modules
cards
application
infrastructure
types
webapp
···
7
7
### ✅ What Was Implemented
8
8
9
9
**Infrastructure (100% Complete)**
10
10
+
10
11
- ✅ Created `@semble/types` npm workspace package
11
12
- ✅ Configured npm workspaces in root package.json
12
13
- ✅ Set up TypeScript compilation with proper paths
···
14
15
- ✅ Built and compiled successfully
15
16
16
17
**Shared Types Package** (`src/types/src/api/`)
18
18
+
17
19
- ✅ `common.ts` - User, Pagination, Sorting base types
18
20
- ✅ `requests.ts` - All API request types (30+ types)
19
21
- ✅ `responses.ts` - All API response types (30+ types)
···
21
23
- ✅ Compiles cleanly with TypeScript
22
24
23
25
**Backend Migration (100% Complete)**
26
26
+
24
27
- ✅ **All 8 card query use cases** migrated to `@semble/types`
25
28
- GetCollectionsForUrlUseCase
26
29
- GetGlobalFeedUseCase
···
36
39
- ✅ Removed old DTO directories
37
40
38
41
**Frontend Migration (100% Complete)**
42
42
+
39
43
- ✅ ApiClient.ts imports from `@semble/types`
40
44
- ✅ All client files updated (QueryClient, CardClient, etc.)
41
45
- ✅ Removed old `src/webapp/api-client/types/` directory
···
158
162
## 🔧 Development Workflow
159
163
160
164
### Starting Development
165
165
+
161
166
```bash
162
167
# Terminal 1: Watch and rebuild types on changes
163
168
npm run dev:types
···
170
175
```
171
176
172
177
### Making Type Changes
178
178
+
173
179
1. Edit files in `src/types/src/api/`
174
180
2. Types package auto-rebuilds (if dev:types is running)
175
181
3. Both backend and frontend see changes immediately
176
182
4. TypeScript catches any mismatches
177
183
178
184
### Example: Adding a New Endpoint
185
185
+
179
186
```typescript
180
187
// 1. Add types to src/types/src/api/requests.ts
181
188
export interface CreateCommentRequest {
···
218
225
## 📚 Reference Implementations
219
226
220
227
### Example Use Case
228
228
+
221
229
`src/modules/cards/application/useCases/queries/GetCollectionsForUrlUseCase.ts`
230
230
+
222
231
```typescript
223
232
import { GetCollectionsForUrlResponse, Collection } from '@semble/types';
224
233
···
231
240
232
241
return ok({
233
242
collections: enrichedCollections,
234
234
-
pagination: { /* ... */ },
235
235
-
sorting: { /* ... */ },
243
243
+
pagination: {
244
244
+
/* ... */
245
245
+
},
246
246
+
sorting: {
247
247
+
/* ... */
248
248
+
},
236
249
});
237
250
}
238
251
}
239
252
```
240
253
241
254
### Example Controller with Zod
255
255
+
242
256
`src/modules/cards/infrastructure/http/controllers/GetCollectionsForUrlController.ts`
257
257
+
243
258
```typescript
244
259
import { z } from 'zod';
245
260
import { GetCollectionsForUrlResponse } from '@semble/types';
···
264
279
```
265
280
266
281
### Example Frontend Usage
282
282
+
267
283
`src/webapp/api-client/ApiClient.ts`
284
284
+
268
285
```typescript
269
286
import { GetCollectionsForUrlParams, GetCollectionsForUrlResponse } from '@semble/types';
270
287
···
281
298
## 🎯 Future Enhancements (Optional)
282
299
283
300
### Short Term
301
301
+
284
302
- [ ] Add Zod validation to remaining 23 controllers
285
303
- [ ] Create shared Zod utility schemas for pagination/sorting
286
304
- [ ] Add request/response logging middleware
287
305
288
306
### Medium Term
307
307
+
289
308
- [ ] Generate OpenAPI spec from Zod schemas + types
290
309
- [ ] Create API documentation from types
291
310
- [ ] Add integration tests using shared types
292
311
- [ ] Runtime response validation in development mode
293
312
294
313
### Long Term
314
314
+
295
315
- [ ] Type versioning strategy for breaking changes
296
316
- [ ] Generate client SDKs for mobile apps
297
317
- [ ] Publish types to private npm registry
···
323
343
## 📝 Key Files Modified
324
344
325
345
### Created
346
346
+
326
347
- ✅ `src/types/` - Entire @semble/types package
327
348
- ✅ `docs/plan/shared_type_unification.md` - Implementation plan
328
349
- ✅ `docs/shared_types_implementation_status.md` - Status tracking
329
350
- ✅ `IMPLEMENTATION_COMPLETE.md` - This file
330
351
331
352
### Modified
353
353
+
332
354
- ✅ `package.json` - Added workspaces, @semble/types dependency, zod
333
355
- ✅ `src/webapp/package.json` - Added @semble/types dependency
334
356
- ✅ `tsconfig.json` - Added paths for @semble/types
···
340
362
- ✅ All webapp client files - Import from @semble/types
341
363
342
364
### Deleted
365
365
+
343
366
- ✅ `src/modules/cards/application/dtos/` - Moved to @semble/types
344
367
- ✅ `src/modules/user/application/dtos/` - Moved to @semble/types
345
368
- ✅ `src/webapp/api-client/types/` - Moved to @semble/types
···
9
9
**TL;DR**: You already have the important mapper (Domain → Application), and adding another layer (Application → Infrastructure) would just be ceremony when the types are identical.
10
10
11
11
**You're already doing this (correct DDD):**
12
12
+
12
13
```
13
14
Domain Entity → Use Case (maps to DTO) → Controller (passes through) → Frontend
14
15
↓ ↓ ↓
···
18
19
**The shared types represent the Application Layer**, not Infrastructure. Both backend controllers and frontend clients consume the same Application Layer contract - this is **textbook Ports & Adapters pattern**.
19
20
20
21
**When you WOULD need mappers:**
22
22
+
21
23
- API versioning (supporting v1 and v2)
22
24
- Multiple protocols (REST + GraphQL + gRPC)
23
25
- Public API (hiding internal structures)
···
32
34
### What Exists Today
33
35
34
36
**Backend DTOs** (`src/modules/cards/application/dtos/`):
37
37
+
35
38
- `UserProfileDTO`, `UrlCardDTO`, `NoteCardDTO`, `CollectionDTO`
36
39
- `PaginationDTO`, `CardSortingDTO`, `CollectionSortingDTO`
37
40
- `FeedItemDTO`
38
41
- These are used as return types from Use Cases
39
42
40
43
**Frontend Types** (`src/webapp/api-client/types/`):
44
44
+
41
45
- `requests.ts` - Request parameter interfaces
42
46
- `responses.ts` - Response type interfaces (User, UrlCard, Collection, etc.)
43
47
- Nearly identical to backend DTOs (already unified in recent work)
44
48
45
49
**Current Flow**:
50
50
+
46
51
```
47
52
Domain Model → Use Case (returns DTO) → Controller (returns DTO as-is) → Frontend (expects matching type)
48
53
```
···
58
63
### DDD Layered Architecture Review
59
64
60
65
**Classic DDD Layers:**
66
66
+
61
67
1. **Domain Layer**: Entities, Value Objects, Domain Services, Domain Events
62
68
2. **Application Layer**: Use Cases, DTOs, Application Services
63
69
3. **Infrastructure Layer**: Controllers, Repositories, External Services
···
126
132
```
127
133
128
134
**When you need this:**
135
135
+
129
136
- ✅ Public API that needs versioning independent of domain
130
137
- ✅ Domain model significantly different from API representation
131
138
- ✅ Multiple API formats (REST, GraphQL, gRPC) from same domain
···
133
140
- ✅ Large team with separate domain/API teams
134
141
135
142
**Drawbacks:**
143
143
+
136
144
- ❌ High ceremony when DTO ≈ HTTP Response
137
145
- ❌ Boilerplate mapper code
138
146
- ❌ Slower iteration velocity
···
168
176
```
169
177
170
178
**When this is appropriate:**
179
179
+
171
180
- ✅ Monorepo with tight frontend/backend coupling
172
181
- ✅ DTO and HTTP Response are identical (or nearly so)
173
182
- ✅ Private/internal API (not public third-party API)
···
177
186
**Key insight:** The shared types represent the **Application Layer contract**, not Infrastructure. Both the backend Use Case and frontend are clients of this application layer contract.
178
187
179
188
**Drawbacks:**
189
189
+
180
190
- ⚠️ Harder to version API independently
181
191
- ⚠️ Frontend sees application layer types (but this may be fine)
182
192
···
193
203
```
194
204
195
205
**This is an anti-pattern** because:
206
206
+
196
207
- ❌ Use Cases depend on Infrastructure (breaks DDD layering)
197
208
- ❌ Application layer coupled to HTTP representation
198
209
- ❌ Can't reuse Use Cases for non-HTTP interfaces
···
202
213
**Why this is the right choice for your codebase:**
203
214
204
215
1. **You're already mapping Domain → Application:**
216
216
+
205
217
```typescript
206
218
// GetCollectionsForUrlUseCase.ts - lines 122-151
207
219
const enrichedCollections: CollectionDTO[] = await Promise.all(
···
224
236
})
225
237
);
226
238
```
239
239
+
227
240
This is **proper DDD** - the Use Case orchestrates domain objects and produces DTOs.
228
241
229
242
2. **Your DTOs ARE your HTTP responses:**
···
251
264
Add a separate HTTP Response layer if:
252
265
253
266
1. **API Versioning:**
267
267
+
254
268
```typescript
255
269
// v1: { id, name }
256
270
// v2: { id, title } // renamed field
···
262
276
```
263
277
264
278
2. **Different Representations:**
279
279
+
265
280
```typescript
266
281
// REST API: flat structure
267
282
{ collectionId: '123', authorId: '456' }
···
271
286
```
272
287
273
288
3. **Hide Internal Details:**
289
289
+
274
290
```typescript
275
291
// DTO has internal fields
276
292
interface CollectionDTO {
277
293
id: string;
278
294
name: string;
279
279
-
internalAuditLog: AuditEntry[]; // don't expose
295
295
+
internalAuditLog: AuditEntry[]; // don't expose
280
296
}
281
297
282
298
// HTTP response filters
···
301
317
// These are Application Layer DTOs, not Infrastructure types
302
318
303
319
// common.ts - Core domain concepts
304
304
-
export interface User { /* ... */ }
305
305
-
export interface Collection { /* ... */ }
320
320
+
export interface User {
321
321
+
/* ... */
322
322
+
}
323
323
+
export interface Collection {
324
324
+
/* ... */
325
325
+
}
306
326
307
327
// requests.ts - Use Case inputs
308
308
-
export interface GetCollectionsParams { /* ... */ }
328
328
+
export interface GetCollectionsParams {
329
329
+
/* ... */
330
330
+
}
309
331
310
332
// responses.ts - Use Case outputs
311
311
-
export interface GetCollectionsResponse { /* ... */ }
333
333
+
export interface GetCollectionsResponse {
334
334
+
/* ... */
335
335
+
}
312
336
```
313
337
314
338
**Use Cases depend on these types:**
339
339
+
315
340
```typescript
316
341
import { GetCollectionsResponse } from '@semble/types';
317
342
···
323
348
```
324
349
325
350
**Controllers are thin pipes:**
351
351
+
326
352
```typescript
327
353
import { GetCollectionsResponse } from '@semble/types';
328
354
···
335
361
```
336
362
337
363
**Frontend consumes Application Layer contract:**
364
364
+
338
365
```typescript
339
366
import { GetCollectionsResponse } from '@semble/types';
340
367
···
348
375
### Summary
349
376
350
377
**What we're doing:**
378
378
+
351
379
- ✅ Sharing **Application Layer** types between backend and frontend
352
380
- ✅ Use Cases map Domain → Application DTO (proper DDD)
353
381
- ✅ Controllers validate HTTP and pass through DTOs
354
382
- ✅ Single source of truth for application contracts
355
383
356
384
**What we're NOT doing:**
385
385
+
357
386
- ❌ Skipping Domain → Application mapping (we do this!)
358
387
- ❌ Letting infrastructure dictate application types
359
388
- ❌ Breaking DDD layer dependencies
360
389
361
390
**This is pragmatic DDD because:**
391
391
+
362
392
- Domain layer remains pure ✅
363
393
- Application layer defines contracts ✅
364
394
- Infrastructure depends on Application (correct direction) ✅
···
371
401
### Three Levels of Validation
372
402
373
403
#### 1. Controller-Level Validation (Infrastructure Layer)
404
404
+
374
405
**Purpose**: Validate HTTP request structure and types
375
406
**Implementation**: Use Zod schemas at controller entry points
376
407
**Examples**:
408
408
+
377
409
- Is `url` parameter present and a string?
378
410
- Is `page` a positive integer?
379
411
- Are required fields in request body present?
···
404
436
```
405
437
406
438
#### 2. Use Case-Level Validation (Application Layer)
439
439
+
407
440
**Purpose**: Validate business rules and create domain objects
408
441
**Implementation**: Use domain value objects (already doing this!)
409
442
**Examples**:
443
443
+
410
444
- `URL.create(query.url)` - validates URL format using domain rules
411
445
- `CardId.createFromString(id)` - validates ID format
412
446
- Business logic validation (e.g., "user can only have 50 collections")
···
416
450
This layer is **already correct** in the codebase!
417
451
418
452
#### 3. Domain-Level Validation (Domain Layer)
453
453
+
419
454
**Purpose**: Enforce invariants and domain rules
420
455
**Implementation**: Value Objects and Entity constructors
421
456
**Examples**:
457
457
+
422
458
- `URL` value object validates URL format
423
459
- `CollectionName` might enforce length limits
424
460
- `Email` value object validates email format
···
480
516
### Type Organization
481
517
482
518
**`src/types/src/api/common.ts`** - Shared domain concepts:
519
519
+
483
520
```typescript
484
521
export interface User {
485
522
id: string;
···
515
552
```
516
553
517
554
**`src/types/src/api/requests.ts`** - API request types:
555
555
+
518
556
```typescript
519
557
// Base interfaces
520
558
export interface PaginationParams {
···
528
566
}
529
567
530
568
// Query parameter interfaces
531
531
-
export interface GetCollectionsForUrlParams extends PaginationParams, SortingParams {
569
569
+
export interface GetCollectionsForUrlParams
570
570
+
extends PaginationParams,
571
571
+
SortingParams {
532
572
url: string;
533
573
}
534
574
···
543
583
```
544
584
545
585
**`src/types/src/api/responses.ts`** - API response types:
586
586
+
546
587
```typescript
547
588
import { User, Pagination, CardSorting, CollectionSorting } from './common';
548
589
···
594
635
### Phase 0: Prerequisites
595
636
596
637
**Install Zod for validation:**
638
638
+
597
639
```bash
598
640
npm install zod
599
641
```
···
603
645
#### 1.1 Configure Workspace Root
604
646
605
647
Update `package.json`:
648
648
+
606
649
```json
607
650
{
608
651
"name": "semble",
609
609
-
"workspaces": [
610
610
-
"src/types",
611
611
-
"src/webapp"
612
612
-
],
652
652
+
"workspaces": ["src/types", "src/webapp"],
613
653
"scripts": {
614
654
"build:types": "npm run build --workspace=@semble/types",
615
655
"dev:types": "npm run dev --workspace=@semble/types",
···
621
661
#### 1.2 Create Types Package
622
662
623
663
Create `src/types/package.json`:
664
664
+
624
665
```json
625
666
{
626
667
"name": "@semble/types",
···
641
682
```
642
683
643
684
Create `src/types/tsconfig.json`:
685
685
+
644
686
```json
645
687
{
646
688
"compilerOptions": {
···
674
716
#### 2.1 Add Dependency
675
717
676
718
Update root `package.json`:
719
719
+
677
720
```json
678
721
{
679
722
"dependencies": {
···
690
733
**Example: `GetCollectionsForUrlUseCase.ts`**
691
734
692
735
Before:
736
736
+
693
737
```typescript
694
738
import { CollectionDTO, PaginationDTO, CollectionSortingDTO } from '../../dtos';
695
739
···
701
745
```
702
746
703
747
After:
748
748
+
704
749
```typescript
705
750
import { GetCollectionsForUrlResponse } from '@semble/types';
706
751
···
718
763
**Example: `GetCollectionsForUrlController.ts`**
719
764
720
765
Before:
766
766
+
721
767
```typescript
722
768
async executeImpl(req: Request, res: Response): Promise<any> {
723
769
const { url } = req.query;
···
729
775
```
730
776
731
777
After:
778
778
+
732
779
```typescript
733
780
import { z } from 'zod';
734
781
import { GetCollectionsForUrlParams, GetCollectionsForUrlResponse } from '@semble/types';
···
771
818
#### 2.4 Remove Old DTOs
772
819
773
820
Once all use cases and controllers are updated:
821
821
+
774
822
```bash
775
823
rm -rf src/modules/cards/application/dtos/
776
824
rm -rf src/modules/user/application/dtos/
···
781
829
#### 3.1 Add Dependency
782
830
783
831
Update `src/webapp/package.json`:
832
832
+
784
833
```json
785
834
{
786
835
"dependencies": {
···
796
845
**`src/webapp/api-client/ApiClient.ts`**
797
846
798
847
Before:
848
848
+
799
849
```typescript
800
850
import type {
801
851
GetCollectionsForUrlParams,
···
804
854
```
805
855
806
856
After:
857
857
+
807
858
```typescript
808
859
import type {
809
860
GetCollectionsForUrlParams,
···
822
873
#### 4.1 Add Development Scripts
823
874
824
875
Update root `package.json`:
876
876
+
825
877
```json
826
878
{
827
879
"scripts": {
···
838
890
#### 4.2 Update Backend tsconfig.json
839
891
840
892
Ensure backend can resolve workspace packages:
893
893
+
841
894
```json
842
895
{
843
896
"compilerOptions": {
···
851
904
### Phase 5: Testing & Validation
852
905
853
906
#### 5.1 Type Checking
907
907
+
854
908
```bash
855
909
npm run type-check # Check backend
856
910
npm run type-check --workspace=@semble/webapp # Check frontend
857
911
```
858
912
859
913
#### 5.2 Build Everything
914
914
+
860
915
```bash
861
916
npm run build:all
862
917
```
863
918
864
919
#### 5.3 Runtime Testing
920
920
+
865
921
- Start dev servers: `npm run dev`
866
922
- Test each modified endpoint
867
923
- Verify request validation works (try invalid requests)
···
870
926
## Migration Checklist
871
927
872
928
### Phase 1: Shared Types Package ✅
929
929
+
873
930
- [ ] Update root `package.json` with workspaces configuration
874
931
- [ ] Create `src/types/package.json`
875
932
- [ ] Create `src/types/tsconfig.json`
···
882
939
- [ ] Verify build output in `src/types/dist/`
883
940
884
941
### Phase 2: Backend Migration ✅
942
942
+
885
943
- [ ] Install zod: `npm install zod`
886
944
- [ ] Add `@semble/types` to root dependencies
887
945
- [ ] Run `npm install` to link workspace
···
898
956
- [ ] Fix any type errors
899
957
900
958
### Phase 3: Frontend Migration ✅
959
959
+
901
960
- [ ] Add `@semble/types` to `src/webapp/package.json`
902
961
- [ ] Run `npm install` in webapp
903
962
- [ ] Update `src/webapp/api-client/ApiClient.ts` imports
···
908
967
- [ ] Fix any type errors
909
968
910
969
### Phase 4: Development Workflow ✅
970
970
+
911
971
- [ ] Add dev scripts to root package.json
912
972
- [ ] Test concurrent development: `npm run dev`
913
973
- [ ] Make a test change to shared types
···
915
975
- [ ] Test build pipeline: `npm run build:all`
916
976
917
977
### Phase 5: Testing ✅
978
978
+
918
979
- [ ] Backend tests pass
919
980
- [ ] Frontend tests pass
920
981
- [ ] Manual testing of key endpoints:
···
931
992
### High Priority (Core Queries)
932
993
933
994
**Use Cases:**
995
995
+
934
996
- `src/modules/cards/application/useCases/queries/GetCollectionsForUrlUseCase.ts`
935
997
- `src/modules/cards/application/useCases/queries/GetCollectionsUseCase.ts`
936
998
- `src/modules/cards/application/useCases/queries/GetLibrariesForCardUseCase.ts`
···
941
1003
- `src/modules/feeds/application/useCases/queries/GetGlobalFeedUseCase.ts`
942
1004
943
1005
**Controllers:**
1006
1006
+
944
1007
- All controllers in `src/modules/cards/infrastructure/http/controllers/`
945
1008
- `src/modules/feeds/infrastructure/http/controllers/GetGlobalFeedController.ts`
946
1009
947
1010
**Frontend:**
1011
1011
+
948
1012
- `src/webapp/api-client/ApiClient.ts`
949
1013
- All files in `src/webapp/api-client/clients/`
950
1014
951
1015
### Medium Priority (Commands)
952
1016
953
1017
**Use Cases:**
1018
1018
+
954
1019
- `src/modules/cards/application/useCases/commands/AddUrlToLibraryUseCase.ts`
955
1020
- `src/modules/cards/application/useCases/commands/CreateCollectionUseCase.ts`
956
1021
- ... (other command use cases)
···
964
1029
## Best Practices
965
1030
966
1031
### Type Naming Conventions
1032
1032
+
967
1033
- **Requests**: `{Verb}{Resource}Request` (e.g., `AddUrlToLibraryRequest`)
968
1034
- **Params**: `Get{Resource}Params` (e.g., `GetCollectionsForUrlParams`)
969
1035
- **Responses**: `{Verb}{Resource}Response` (e.g., `GetCollectionsForUrlResponse`)
···
972
1038
### Validation Patterns
973
1039
974
1040
**Controller validation (structure & types):**
1041
1041
+
975
1042
```typescript
976
1043
const schema = z.object({
977
1044
requiredString: z.string().min(1),
···
986
1053
```
987
1054
988
1055
**Use case validation (business rules):**
1056
1056
+
989
1057
```typescript
990
1058
const urlResult = URL.create(params.url);
991
1059
if (urlResult.isErr()) {
···
994
1062
```
995
1063
996
1064
### Development Workflow
1065
1065
+
997
1066
1. **Always run types in watch mode** during development: `npm run dev:types`
998
1067
2. **Make type changes first** before implementing features
999
1068
3. **Type-check frequently**: `npm run type-check`
···
1004
1073
### What We're NOT Doing (And Why That's OK)
1005
1074
1006
1075
❌ **Separate DTO and HTTP Response mapping layer**:
1076
1076
+
1007
1077
- Reason: Application DTOs and HTTP responses are identical
1008
1078
- Reality: Controllers would just be identity mappers (pointless ceremony)
1009
1079
- When to add: If you need API versioning, different client representations, or hide internal fields
1010
1080
- See "When Would You Need Mappers?" section above for specific scenarios
1011
1081
1012
1082
❌ **Runtime validation of Use Case outputs**:
1083
1083
+
1013
1084
- Reason: TypeScript compilation guarantees response shape from Use Cases
1014
1085
- Alternative: Could add Zod validation of DTO construction, but not needed initially
1015
1086
- When to add: If you have bugs where Use Cases return malformed DTOs
1016
1087
1017
1088
❌ **OpenAPI schema generation**:
1089
1089
+
1018
1090
- Reason: Can add later if needed (Zod schemas make this easy)
1019
1091
- Benefit: Shared types + Zod schemas = future OpenAPI generation is trivial
1020
1092
- When to add: When you want API documentation, client SDK generation, or contract testing
1021
1093
1022
1094
❌ **Separate versioning of types package**:
1095
1095
+
1023
1096
- Reason: Monorepo with synchronized deploys (frontend/backend always in sync)
1024
1097
- When to add: If you publish a public API or have multiple clients on different versions
1025
1098
- Note: For now, Git commits provide version history
1026
1099
1027
1100
❌ **Multiple API representations (REST, GraphQL, gRPC)**:
1101
1101
+
1028
1102
- Reason: Only building REST API currently
1029
1103
- When to add: When you need GraphQL, gRPC, or other protocols (then add mappers)
1030
1104
···
1049
1123
✅ **Type safety across boundaries**: Shared Application contracts prevent drift
1050
1124
1051
1125
The key insight: **Sharing Application Layer types between backend and frontend is not a DDD violation** when:
1126
1126
+
1052
1127
1. Both are clients of the same Application Layer
1053
1128
2. They deploy together (monorepo)
1054
1129
3. The Application DTOs appropriately abstract the Domain Model
···
1056
1131
## Troubleshooting
1057
1132
1058
1133
### "Cannot find module '@semble/types'"
1134
1134
+
1059
1135
```bash
1060
1136
# From root
1061
1137
npm install
···
1063
1139
```
1064
1140
1065
1141
### Types package not updating
1142
1142
+
1066
1143
```bash
1067
1144
# Restart types watch mode
1068
1145
npm run dev:types
1069
1146
```
1070
1147
1071
1148
### Import path errors in IDE
1149
1149
+
1072
1150
- Restart TypeScript server in your IDE
1073
1151
- Check `tsconfig.json` paths configuration
1074
1152
- Verify `node_modules/@semble/types` symlink exists
1075
1153
1076
1154
### Validation errors not showing
1155
1155
+
1077
1156
- Check Zod schema matches the expected request shape
1078
1157
- Use `.safeParse()` not `.parse()` to get detailed errors
1079
1158
- Log `validation.error.format()` for debugging
···
1081
1160
## Future Enhancements
1082
1161
1083
1162
### Short Term
1163
1163
+
1084
1164
- [ ] Add Zod schemas for all controllers
1085
1165
- [ ] Create shared Zod utilities for common patterns (pagination, sorting)
1086
1166
- [ ] Add request/response logging middleware
1087
1167
1088
1168
### Medium Term
1169
1169
+
1089
1170
- [ ] Generate OpenAPI spec from shared types
1090
1171
- [ ] Create API documentation from types
1091
1172
- [ ] Add integration tests using shared types
1092
1173
- [ ] Create type-safe test factories
1093
1174
1094
1175
### Long Term
1176
1176
+
1095
1177
- [ ] Publish types package to private npm registry
1096
1178
- [ ] Implement breaking change detection (type diff checks)
1097
1179
- [ ] Add runtime response validation in development mode
···
1102
1184
### What We Preserved (Proper DDD)
1103
1185
1104
1186
✅ **Domain Layer Purity**:
1187
1187
+
1105
1188
- Domain entities, value objects, and domain services have no external dependencies
1106
1189
- Domain models encapsulate business logic and invariants
1107
1190
- Domain layer doesn't know about HTTP, DTOs, or frontend
1108
1191
1109
1192
✅ **Application Layer Orchestration**:
1193
1193
+
1110
1194
- Use Cases orchestrate domain objects to fulfill application requirements
1111
1195
- Use Cases map rich domain models → simple DTOs (proper anti-corruption layer)
1112
1196
- DTOs hide domain complexity from external consumers
1113
1197
- Application layer defines the contract for consumers (backend and frontend)
1114
1198
1115
1199
✅ **Infrastructure Layer Dependency Direction**:
1200
1200
+
1116
1201
- Controllers depend on Application types (correct: Infrastructure → Application)
1117
1202
- Controllers do NOT define types that Application depends on (would be wrong)
1118
1203
- Infrastructure implements Application contracts, not the other way around
1119
1204
1120
1205
✅ **Three-Tier Validation Hierarchy**:
1206
1206
+
1121
1207
- **Domain**: Invariants enforced in value objects and entities
1122
1208
- **Application**: Business rules validated in use cases
1123
1209
- **Infrastructure**: HTTP request structure validated in controllers
1124
1210
1125
1211
✅ **Separation of Concerns**:
1212
1212
+
1126
1213
- Domain logic in entities/value objects
1127
1214
- Application logic in use cases
1128
1215
- HTTP transport details in controllers
···
1131
1218
### What's Actually Happening (Not a Compromise)
1132
1219
1133
1220
✅ **Shared Application Layer Types**:
1221
1221
+
1134
1222
- `@semble/types` represents the **Application Layer contract**
1135
1223
- Backend Use Cases produce these contracts
1136
1224
- Frontend API Client consumes these contracts
1137
1225
- Both are clients of the Application Layer (this is correct DDD!)
1138
1226
1139
1227
✅ **Controllers as Thin Adapters**:
1228
1228
+
1140
1229
- Controllers adapt HTTP → Application Layer (parse request, call use case)
1141
1230
- Controllers adapt Application Layer → HTTP (serialize DTO to JSON)
1142
1231
- No additional mapping needed when DTO shape = HTTP response shape
···
1145
1234
### Why This Is Good DDD (Not Just "OK for Startups")
1146
1235
1147
1236
**From Eric Evans / Martin Fowler:**
1237
1237
+
1148
1238
- DTOs exist to cross architectural boundaries ✅ (we do this)
1149
1239
- Application Layer should be independent of delivery mechanism ✅ (it is)
1150
1240
- Infrastructure should depend on Application, not vice versa ✅ (correct)
1151
1241
- Shared kernel is acceptable when bounded contexts align ✅ (monorepo, single app)
1152
1242
1153
1243
**From Hexagonal Architecture (Ports & Adapters):**
1244
1244
+
1154
1245
- Application Layer defines "ports" (interfaces/contracts) ✅ (`@semble/types`)
1155
1246
- Infrastructure provides "adapters" (controllers, API clients) ✅ (thin HTTP adapters)
1156
1247
- Multiple adapters can implement same port ✅ (backend controller, frontend client)
1157
1248
- This is the **textbook pattern**!
1158
1249
1159
1250
**When you'd need an additional mapping layer:**
1251
1251
+
1160
1252
1. **API Versioning**: Supporting v1 and v2 simultaneously
1161
1253
2. **Different Protocols**: REST + GraphQL + gRPC from same Application Layer
1162
1254
3. **Public API**: Need to hide internal structures
···
1198
1290
The quality of this architecture depends on whether your DTOs properly abstract the domain:
1199
1291
1200
1292
✅ **Good DTO Design** (what you have):
1293
1293
+
1201
1294
```typescript
1202
1295
// Domain: Rich model with behavior
1203
1296
class Collection {
1204
1204
-
collectionId: CollectionId; // Value object
1205
1205
-
name: CollectionName; // Value object with validation
1206
1206
-
authorId: UserId; // Value object
1207
1207
-
description?: CollectionDescription; // Value object
1208
1208
-
cardIds: CardId[]; // List of value objects
1209
1209
-
addCard(cardId: CardId) { /* logic */ }
1210
1210
-
removeCard(cardId: CardId) { /* logic */ }
1297
1297
+
collectionId: CollectionId; // Value object
1298
1298
+
name: CollectionName; // Value object with validation
1299
1299
+
authorId: UserId; // Value object
1300
1300
+
description?: CollectionDescription; // Value object
1301
1301
+
cardIds: CardId[]; // List of value objects
1302
1302
+
addCard(cardId: CardId) {
1303
1303
+
/* logic */
1304
1304
+
}
1305
1305
+
removeCard(cardId: CardId) {
1306
1306
+
/* logic */
1307
1307
+
}
1211
1308
}
1212
1309
1213
1310
// DTO: Simple data structure for transfer
1214
1311
interface Collection {
1215
1215
-
id: string; // Unwrapped value
1216
1216
-
name: string; // Unwrapped value
1217
1217
-
author: User; // Enriched relationship
1218
1218
-
description?: string; // Unwrapped value
1219
1219
-
cardCount: number; // Computed property
1220
1220
-
createdAt: string; // ISO string
1221
1221
-
updatedAt: string; // ISO string
1312
1312
+
id: string; // Unwrapped value
1313
1313
+
name: string; // Unwrapped value
1314
1314
+
author: User; // Enriched relationship
1315
1315
+
description?: string; // Unwrapped value
1316
1316
+
cardCount: number; // Computed property
1317
1317
+
createdAt: string; // ISO string
1318
1318
+
updatedAt: string; // ISO string
1222
1319
}
1223
1320
```
1224
1321
1225
1322
This is **good separation**:
1323
1323
+
1226
1324
- Domain has rich behavior, validation, invariants
1227
1325
- DTO is simple, serializable, consumer-friendly
1228
1326
- Use Case does the mapping (proper layer responsibility)
1229
1327
1230
1328
❌ **Bad DTO Design** (anti-pattern):
1329
1329
+
1231
1330
```typescript
1232
1331
// Exposing domain internals
1233
1332
interface CollectionDTO {
1234
1234
-
collectionId: CollectionId; // WRONG: exposing domain value object
1235
1235
-
_aggregateVersion: number; // WRONG: exposing internal details
1333
1333
+
collectionId: CollectionId; // WRONG: exposing domain value object
1334
1334
+
_aggregateVersion: number; // WRONG: exposing internal details
1236
1335
domainEvents: DomainEvent[]; // WRONG: leaking domain events
1237
1336
}
1238
1337
```
···
1402
1501
```
1403
1502
1404
1503
### Key Changes
1504
1504
+
1405
1505
1. ✅ Single `Collection` type (not duplicated)
1406
1506
2. ✅ Single `GetCollectionsForUrlResponse` type
1407
1507
3. ✅ Controller validates with Zod schema
···
1422
1522
- **Stays simple** and avoids overengineering
1423
1523
1424
1524
The result is a **barebones but reliable** type system that:
1525
1525
+
1425
1526
- Catches errors at compile time
1426
1527
- Validates requests at runtime
1427
1528
- Maintains a single source of truth
···
54
54
{
55
55
"name": "annos",
56
56
"version": "1.0.0",
57
57
-
"workspaces": [
58
58
-
"src/types",
59
59
-
"src/webapp",
60
60
-
"."
61
61
-
],
57
57
+
"workspaces": ["src/types", "src/webapp", "."],
62
58
"scripts": {
63
59
"build:types": "npm run build --workspace=@annos/types",
64
60
"dev:types": "npm run dev --workspace=@annos/types",
···
80
76
"description": "Shared TypeScript types for Annos API",
81
77
"main": "dist/index.js",
82
78
"types": "dist/index.d.ts",
83
83
-
"files": [
84
84
-
"dist/**/*"
85
85
-
],
79
79
+
"files": ["dist/**/*"],
86
80
"scripts": {
87
81
"build": "tsc",
88
82
"dev": "tsc --watch",
···
125
119
{
126
120
"name": "@annos/webapp",
127
121
"dependencies": {
128
128
-
"@annos/types": "workspace:*",
122
122
+
"@annos/types": "workspace:*"
129
123
// ... existing dependencies
130
124
}
131
125
}
···
138
132
Move and organize existing webapp types into the shared package:
139
133
140
134
**src/types/src/api/common.ts:**
135
135
+
141
136
```typescript
142
137
export interface User {
143
138
id: string;
···
173
168
```
174
169
175
170
**src/types/src/api/requests.ts:**
171
171
+
176
172
```typescript
177
173
// Copy all request types from src/webapp/api-client/types/requests.ts
178
174
export interface PaginationParams {
···
189
185
```
190
186
191
187
**src/types/src/api/responses.ts:**
188
188
+
192
189
```typescript
193
193
-
import { User, Pagination, CardSorting, CollectionSorting, FeedPagination } from './common';
190
190
+
import {
191
191
+
User,
192
192
+
Pagination,
193
193
+
CardSorting,
194
194
+
CollectionSorting,
195
195
+
FeedPagination,
196
196
+
} from './common';
194
197
195
198
// Copy all response types from src/webapp/api-client/types/responses.ts
196
199
export interface UrlCard {
···
204
207
```
205
208
206
209
**src/types/src/api/index.ts:**
210
210
+
207
211
```typescript
208
212
export * from './common';
209
213
export * from './requests';
···
211
215
```
212
216
213
217
**src/types/src/index.ts:**
218
218
+
214
219
```typescript
215
220
export * from './api';
216
221
```
···
242
247
} from './types/responses';
243
248
244
249
// NEW:
245
245
-
import type {
246
246
-
GetUrlCardsResponse,
247
247
-
AddUrlToLibraryRequest,
248
248
-
} from '@annos/types';
250
250
+
import type { GetUrlCardsResponse, AddUrlToLibraryRequest } from '@annos/types';
249
251
```
250
252
251
253
#### Step 3.3: Remove Old Type Files
···
276
278
277
279
export class GetUrlCardsUseCase {
278
280
async execute(
279
279
-
query: GetUrlCardsQuery
280
280
-
): Promise<Result<GetUrlCardsResponse, ValidationError | AppError.UnexpectedError>> {
281
281
+
query: GetUrlCardsQuery,
282
282
+
): Promise<
283
283
+
Result<GetUrlCardsResponse, ValidationError | AppError.UnexpectedError>
284
284
+
> {
281
285
// Implementation must return GetUrlCardsResponse type
282
286
return ok({
283
287
cards: enrichedCards,
···
356
360
Add Zod schemas for runtime validation:
357
361
358
362
**src/types/src/validation/index.ts:**
363
363
+
359
364
```typescript
360
365
import { z } from 'zod';
361
366
···
385
390
## Migration Checklist
386
391
387
392
### Phase 1: Infrastructure ✅
393
393
+
388
394
- [ ] Update root `package.json` with workspaces
389
395
- [ ] Create `src/types/package.json`
390
396
- [ ] Create `src/types/tsconfig.json`
···
392
398
- [ ] Run `npm install` to setup workspace
393
399
394
400
### Phase 2: Type Migration ✅
401
401
+
395
402
- [ ] Create `src/types/src/api/common.ts`
396
403
- [ ] Create `src/types/src/api/requests.ts`
397
404
- [ ] Create `src/types/src/api/responses.ts`
···
400
407
- [ ] Build shared types: `npm run build:types`
401
408
402
409
### Phase 3: Frontend Migration ✅
410
410
+
403
411
- [ ] Update all imports in webapp to use `@annos/types`
404
412
- [ ] Remove old type files: `rm -rf src/webapp/api-client/types/`
405
413
- [ ] Test webapp compilation: `npm run type-check --workspace=@annos/webapp`
406
414
407
415
### Phase 4: Backend Migration ✅
416
416
+
408
417
- [ ] Add shared types dependency to root package
409
418
- [ ] Update use cases to import and return shared types
410
419
- [ ] Update controllers to use shared types
411
420
- [ ] Test backend compilation: `npm run type-check`
412
421
413
422
### Phase 5: Development Setup ✅
423
423
+
414
424
- [ ] Add development scripts to root package.json
415
425
- [ ] Test concurrent development: `npm run dev`
416
426
- [ ] Verify hot reload works for type changes
417
427
418
428
### Phase 6: Validation ✅
429
429
+
419
430
- [ ] Run full type check across all packages
420
431
- [ ] Test API endpoints return correct types
421
432
- [ ] Add runtime validation (optional)
···
424
435
## Best Practices
425
436
426
437
### Type Naming Conventions
438
438
+
427
439
- **Requests**: `{Action}{Resource}Request` (e.g., `GetUrlCardsRequest`)
428
440
- **Responses**: `{Action}{Resource}Response` (e.g., `GetUrlCardsResponse`)
429
441
- **Common types**: Descriptive names (e.g., `User`, `Pagination`)
430
442
431
443
### Development Workflow
444
444
+
432
445
1. **Always run shared types in watch mode** during development
433
446
2. **Make type changes first** before implementing features
434
447
3. **Use TypeScript strict mode** to catch issues early
435
448
4. **Version shared types** when making breaking changes
436
449
437
450
### Error Handling
451
451
+
438
452
- Define consistent error response types
439
453
- Use discriminated unions for different error types
440
454
- Include error codes and messages in shared types
···
3
3
## ✅ Successfully Completed
4
4
5
5
### Infrastructure Setup
6
6
+
6
7
- ✅ Created `@semble/types` package at `src/types/`
7
8
- ✅ Configured npm workspaces in root `package.json`
8
9
- ✅ Set up TypeScript compilation for types package
···
11
12
- ✅ Installed zod for validation (version 3.22.4)
12
13
13
14
### Shared Types Package (`src/types/src/api/`)
15
15
+
14
16
- ✅ `common.ts` - User, Pagination, Sorting interfaces
15
17
- ✅ `requests.ts` - All request parameter types
16
18
- ✅ `responses.ts` - All response types
17
19
- ✅ Built successfully with TypeScript
18
20
19
21
### Backend Migration (Cards/Feeds Modules)
22
22
+
20
23
- ✅ **GetCollectionsForUrlUseCase** - Uses `GetCollectionsForUrlResponse` from `@semble/types`
21
24
- ✅ **GetGlobalFeedUseCase** - Uses `GetGlobalFeedResponse` from `@semble/types`
22
25
- ✅ **GetCollectionsForUrlController** - Added Zod validation schema
···
24
27
- ✅ Removed old `src/modules/cards/application/dtos/` directory
25
28
26
29
### Frontend Migration
30
30
+
27
31
- ✅ **ApiClient.ts** - Imports all types from `@semble/types`
28
32
- ✅ All client files (`QueryClient`, `CardClient`, etc.) - Updated to import from `@semble/types`
29
33
- ✅ Removed old `src/webapp/api-client/types/` directory
···
31
35
## ⚠️ Remaining Work (Not Critical for Core Functionality)
32
36
33
37
### Use Cases to Migrate
38
38
+
34
39
These use cases still import from old `../../dtos` and need migration:
35
40
36
41
**Cards Module:**
42
42
+
37
43
- `GetCollectionsUseCase.ts`
38
44
- `GetLibrariesForCardUseCase.ts`
39
45
- `GetLibrariesForUrlUseCase.ts`
···
42
48
- `GetUrlStatusForMyLibraryUseCase.ts`
43
49
44
50
**Pattern to follow:**
51
51
+
45
52
```typescript
46
53
// OLD
47
54
import { CollectionDTO, PaginationDTO } from '../../dtos';
···
52
59
```
53
60
54
61
### User Module DTOs
62
62
+
55
63
The following files reference deleted User DTOs and need new types added to `@semble/types`:
56
64
57
65
**Missing types to add:**
66
66
+
58
67
- `OAuthCallbackDTO` (used by OAuth flows)
59
68
- `TokenDTO` (used by token services)
60
69
- `UserDTO` (used by user mappers)
61
70
- `LoginWithAppPasswordDTO` (used by login use case)
62
71
63
72
**Affected files:**
73
73
+
64
74
- `src/modules/atproto/infrastructure/services/AtProtoOAuthProcessor.ts`
65
75
- `src/modules/atproto/infrastructure/services/FakeAtProtoOAuthProcessor.ts`
66
76
- `src/modules/user/application/mappers/UserMap.ts`
···
73
83
Add these types to `src/types/src/api/responses.ts` or create a new `auth.ts` file.
74
84
75
85
### Controllers to Add Zod Validation
86
86
+
76
87
All other controllers in `src/modules/cards/infrastructure/http/controllers/` should follow the same pattern:
77
88
78
89
```typescript
···
94
105
## 🎯 What's Working Right Now
95
106
96
107
### End-to-End Type Safety
108
108
+
97
109
1. **Frontend** → API request using types from `@semble/types`
98
110
2. **Controller** → Validates request with Zod schema
99
111
3. **Use Case** → Returns response typed as `@semble/types` interface
···
101
113
5. **Frontend** → Receives response with full type safety
102
114
103
115
### Example Flow (Working)
116
116
+
104
117
```
105
118
Frontend: getCollectionsForUrl(params: GetCollectionsForUrlParams)
106
119
↓
···
112
125
```
113
126
114
127
### Compile Status
128
128
+
115
129
- **Frontend**: ✅ Should compile (needs verification with `npm run type-check --workspace=@semble/webapp`)
116
130
- **Backend**: ⚠️ 20 type errors (all in non-migrated use cases and user module)
117
131
- **Types Package**: ✅ Compiles successfully
···
121
135
### For Each Remaining Use Case
122
136
123
137
1. **Update imports:**
138
138
+
124
139
```typescript
125
140
// Replace
126
141
import { SomeDTO } from '../../dtos';
···
129
144
```
130
145
131
146
2. **Update return type:**
147
147
+
132
148
```typescript
133
149
// Replace
134
150
export interface GetSomeResult {
···
144
160
### For User Module
145
161
146
162
1. **Add missing types to `@semble/types`:**
163
163
+
147
164
```typescript
148
165
// src/types/src/api/auth.ts
149
166
export interface TokenResponse {
···
209
226
## 📚 Reference Implementation
210
227
211
228
See these files for the pattern to follow:
229
229
+
212
230
- **Use Case**: `src/modules/cards/application/useCases/queries/GetCollectionsForUrlUseCase.ts`
213
231
- **Controller**: `src/modules/cards/infrastructure/http/controllers/GetCollectionsForUrlController.ts`
214
232
- **Frontend**: `src/webapp/api-client/ApiClient.ts`
···
8
8
import { IProfileService } from 'src/modules/cards/domain/services/IProfileService';
9
9
import { DIDOrHandle } from 'src/modules/atproto/domain/DIDOrHandle';
10
10
import { IIdentityResolutionService } from 'src/modules/atproto/domain/services/IIdentityResolutionService';
11
11
-
import { CollectionDTO, PaginationDTO, CollectionSortingDTO } from '@semble/types';
11
11
+
import {
12
12
+
CollectionDTO,
13
13
+
PaginationDTO,
14
14
+
CollectionSortingDTO,
15
15
+
} from '@semble/types';
12
16
13
17
export interface GetCollectionsQuery {
14
18
curatorId: string;
···
13
13
url: z.string().min(1, 'URL is required'),
14
14
page: z.coerce.number().int().positive().optional(),
15
15
limit: z.coerce.number().int().positive().max(100).optional(),
16
16
-
sortBy: z
17
17
-
.enum(['name', 'createdAt', 'updatedAt', 'cardCount'])
18
18
-
.optional(),
16
16
+
sortBy: z.enum(['name', 'createdAt', 'updatedAt', 'cardCount']).optional(),
19
17
sortOrder: z.enum(['asc', 'desc']).optional(),
20
18
});
21
19
···
1
1
-
import { User, Pagination, CardSorting, CollectionSorting, FeedPagination } from './common';
1
1
+
import {
2
2
+
User,
3
3
+
Pagination,
4
4
+
CardSorting,
5
5
+
CollectionSorting,
6
6
+
FeedPagination,
7
7
+
} from './common';
2
8
3
9
// Command response types
4
10
export interface AddUrlToLibraryResponse {
···
59
59
// Get existing collections for this card (filtered by current user)
60
60
const existingCollections = useMemo(() => {
61
61
if (!existingCardCollections || !userId) return [];
62
62
-
return existingCardCollections.map((c: any) => ({ ...c, authorId: userId }));
62
62
+
return existingCardCollections.map((c: any) => ({
63
63
+
...c,
64
64
+
authorId: userId,
65
65
+
}));
63
66
}, [existingCardCollections, userId]);
64
67
65
68
const handleSubmit = async (e: React.FormEvent) => {
···
12
12
Tooltip,
13
13
} from '@mantine/core';
14
14
import Link from 'next/link';
15
15
-
import { GetUrlStatusForMyLibraryResponse, UrlCard, Collection } from '@/api-client';
15
15
+
import {
16
16
+
GetUrlStatusForMyLibraryResponse,
17
17
+
UrlCard,
18
18
+
Collection,
19
19
+
} from '@/api-client';
16
20
import { BiCollection } from 'react-icons/bi';
17
21
import { LuLibrary } from 'react-icons/lu';
18
22
import { getDomain } from '@/lib/utils/link';
···
36
36
) : (
37
37
<Fragment>
38
38
added to{' '}
39
39
-
{displayedCollections.map((collection: Collection, index: number) => (
40
40
-
<span key={collection.id}>
41
41
-
<Anchor
42
42
-
component={Link}
43
43
-
href={`/profile/${collection.author.handle}/collections/${getRecordKey(collection.uri!)}`}
44
44
-
c="grape"
45
45
-
fw={500}
46
46
-
>
47
47
-
{collection.name}
48
48
-
</Anchor>
49
49
-
{index < displayedCollections.length - 1 ? ', ' : ''}
50
50
-
</span>
51
51
-
))}
39
39
+
{displayedCollections.map(
40
40
+
(collection: Collection, index: number) => (
41
41
+
<span key={collection.id}>
42
42
+
<Anchor
43
43
+
component={Link}
44
44
+
href={`/profile/${collection.author.handle}/collections/${getRecordKey(collection.uri!)}`}
45
45
+
c="grape"
46
46
+
fw={500}
47
47
+
>
48
48
+
{collection.name}
49
49
+
</Anchor>
50
50
+
{index < displayedCollections.length - 1 ? ', ' : ''}
51
51
+
</span>
52
52
+
),
53
53
+
)}
52
54
{remainingCount > 0 &&
53
55
` and ${remainingCount} other collection${remainingCount > 1 ? 's' : ''}`}
54
56
</Fragment>
···
1
1
-
import type { GetLibrariesForUrlResponse } from '@/api-client/types';
1
1
+
import { GetLibrariesForUrlResponse } from '@/api-client';
2
2
import { getRelativeTime } from '@/lib/utils/time';
3
3
import { Avatar, Card, Group, Stack, Text } from '@mantine/core';
4
4
import Link from 'next/link';