A social knowledge tool for researchers built on ATProto
1# API Client Architecture 2 3This document outlines the recommended approach for creating an API client that abstracts away the details of interacting with the backend API. 4 5## Recommended Approach: Separate Shared Package 6 7Create a **separate npm package** (e.g., `@yourapp/api-client` or `@yourapp/shared`) that contains: 8 91. **API Client** - The abstraction layer with methods like `addUrlToLibrary()` 102. **Shared Types** - DTOs, response types, error types 113. **Type Guards/Validators** - Runtime type checking utilities 12 13### Why This Approach? 14 15**Pros:** 16 17- **Single source of truth** for API contracts 18- **Type safety** between frontend and backend 19- **Reusable** across multiple frontends (web, mobile, etc.) 20- **Versioned** - can manage API changes cleanly 21- **Testable** in isolation 22 23**Cons:** 24 25- Additional build/publish complexity 26- Need to manage package versioning 27 28## Package Structure 29 30``` 31packages/ 32├── api-client/ 33│ ├── src/ 34│ │ ├── client/ 35│ │ │ └── ApiClient.ts 36│ │ ├── types/ 37│ │ │ ├── cards.ts 38│ │ │ ├── collections.ts 39│ │ │ └── common.ts 40│ │ └── index.ts 41│ └── package.json 42├── backend/ 43└── frontend/ 44``` 45 46## Example API Client Structure 47 48```typescript 49// packages/api-client/src/types/cards.ts 50export interface AddUrlToLibraryRequest { 51 url: string; 52 note?: string; 53 collectionIds?: string[]; 54} 55 56export interface AddUrlToLibraryResponse { 57 urlCardId: string; 58 noteCardId?: string; 59} 60 61// packages/api-client/src/client/ApiClient.ts 62export class ApiClient { 63 constructor( 64 private baseUrl: string, 65 private getAuthToken: () => string | null, 66 ) {} 67 68 async addUrlToLibrary( 69 request: AddUrlToLibraryRequest, 70 ): Promise<AddUrlToLibraryResponse> { 71 const response = await this.post('/api/cards/library/urls', request); 72 return response.json(); 73 } 74 75 async createCollection( 76 request: CreateCollectionRequest, 77 ): Promise<CreateCollectionResponse> { 78 const response = await this.post('/api/collections', request); 79 return response.json(); 80 } 81 82 private async post(endpoint: string, data: any) { 83 const token = this.getAuthToken(); 84 return fetch(`${this.baseUrl}${endpoint}`, { 85 method: 'POST', 86 headers: { 87 'Content-Type': 'application/json', 88 ...(token && { Authorization: `Bearer ${token}` }), 89 }, 90 body: JSON.stringify(data), 91 }); 92 } 93} 94``` 95 96## Type Sharing Strategy 97 98**Extract from your existing backend types:** 99 100- Take the DTO interfaces from your use cases 101- Create clean, frontend-friendly versions 102- Add any additional client-side types needed 103 104```typescript 105// Extract these from your backend use cases: 106export interface AddUrlToLibraryDTO { 107 url: string; 108 note?: string; 109 collectionIds?: string[]; 110 curatorId: string; // This might be handled automatically by the client 111} 112 113// Transform to client-friendly version: 114export interface AddUrlToLibraryRequest { 115 url: string; 116 note?: string; 117 collectionIds?: string[]; 118 // curatorId removed - handled by auth 119} 120``` 121 122## Alternative Approaches 123 124### 1. Frontend-Only Client 125 126- Keep API client in frontend repo 127- Duplicate types or use code generation 128- Simpler but less maintainable 129 130### 2. Backend Exports Client 131 132- Export client from backend 133- Frontend imports it 134- Can work but creates coupling 135 136### 3. Code Generation 137 138- Generate client from OpenAPI spec 139- Tools like `openapi-generator` or `swagger-codegen` 140- Great for large APIs but adds complexity 141 142## Implementation Steps 143 1441. **Create the package structure** 1452. **Extract and clean up types** from your use case DTOs 1463. **Build the ApiClient class** with methods matching your endpoints 1474. **Add error handling** and response transformation 1485. **Set up build/publish pipeline** 1496. **Update frontend to use the client** 150 151## Error Handling Example 152 153```typescript 154export class ApiError extends Error { 155 constructor( 156 message: string, 157 public status: number, 158 public code?: string 159 ) { 160 super(message); 161 this.name = 'ApiError'; 162 } 163} 164 165// In ApiClient 166private async handleResponse<T>(response: Response): Promise<T> { 167 if (!response.ok) { 168 const error = await response.json().catch(() => ({})); 169 throw new ApiError( 170 error.message || 'Request failed', 171 response.status, 172 error.code 173 ); 174 } 175 return response.json(); 176} 177``` 178 179This approach gives you the best balance of maintainability, type safety, and reusability.