whatsapp-cloud 0.0.3 โ†’ 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +11 -0
  3. package/agent_docs/DESIGN.md +707 -0
  4. package/agent_docs/MESSAGES_NAMESPACE_ANALYSIS.md +368 -0
  5. package/agent_docs/NAMING_DECISION.md +78 -0
  6. package/agent_docs/STRUCTURE.md +711 -0
  7. package/agent_docs/messages-namespace-design.md +357 -0
  8. package/package.json +5 -2
  9. package/src/client/HttpClient.ts +122 -0
  10. package/src/client/WhatsAppClient.ts +55 -0
  11. package/src/client/index.ts +2 -0
  12. package/src/errors.ts +58 -0
  13. package/src/index.ts +16 -1
  14. package/src/schemas/accounts/index.ts +1 -0
  15. package/src/schemas/accounts/phone-number.ts +20 -0
  16. package/src/schemas/business/account.ts +43 -0
  17. package/src/schemas/business/index.ts +2 -0
  18. package/src/schemas/client.ts +50 -0
  19. package/src/schemas/debug.ts +25 -0
  20. package/src/schemas/index.ts +5 -0
  21. package/src/schemas/messages/index.ts +2 -0
  22. package/src/schemas/messages/request.ts +82 -0
  23. package/src/schemas/messages/response.ts +19 -0
  24. package/src/services/accounts/AccountsClient.ts +42 -0
  25. package/src/services/accounts/AccountsService.ts +47 -0
  26. package/src/services/accounts/index.ts +2 -0
  27. package/src/services/accounts/methods/list-phone-numbers.ts +16 -0
  28. package/src/services/business/BusinessClient.ts +42 -0
  29. package/src/services/business/BusinessService.ts +47 -0
  30. package/src/services/business/index.ts +3 -0
  31. package/src/services/business/methods/list-accounts.ts +18 -0
  32. package/src/services/index.ts +2 -0
  33. package/src/services/messages/MessagesClient.ts +38 -0
  34. package/src/services/messages/MessagesService.ts +77 -0
  35. package/src/services/messages/index.ts +8 -0
  36. package/src/services/messages/methods/send-image.ts +33 -0
  37. package/src/services/messages/methods/send-location.ts +32 -0
  38. package/src/services/messages/methods/send-reaction.ts +33 -0
  39. package/src/services/messages/methods/send-text.ts +32 -0
  40. package/src/services/messages/utils/build-message-payload.ts +32 -0
  41. package/src/types/accounts/index.ts +1 -0
  42. package/src/types/accounts/phone-number.ts +9 -0
  43. package/src/types/business/account.ts +10 -0
  44. package/src/types/business/index.ts +2 -0
  45. package/src/types/client.ts +8 -0
  46. package/src/types/debug.ts +8 -0
  47. package/src/types/index.ts +5 -0
  48. package/src/types/messages/index.ts +2 -0
  49. package/src/types/messages/request.ts +27 -0
  50. package/src/types/messages/response.ts +7 -0
  51. package/src/utils/zod-error.ts +28 -0
  52. package/tsconfig.json +4 -1
@@ -0,0 +1,711 @@
1
+ # WhatsApp Cloud API SDK - Package Structure
2
+
3
+ ## ๐ŸŽฏ Design Principles
4
+
5
+ 1. **Schemas First** - Zod schemas are the single source of truth, types are inferred
6
+ 2. **AI-Ready** - Zod schemas enable LLM function calling and validation
7
+ 3. **Type Safety** - TypeScript types inferred from schemas for full type safety
8
+ 4. **Discriminated Unions** - Type-safe message/response variants using Zod
9
+ 5. **Best-Practice Structure** - Inspired by Vercel AI SDK & Stripe
10
+ 6. **Namespace Organization** - Clear separation of concerns
11
+
12
+ ## ๐Ÿ“ Complete Package Structure
13
+
14
+ ```
15
+ whatsapp-cloud/
16
+ โ”œโ”€โ”€ src/
17
+ โ”‚ โ”œโ”€โ”€ index.ts # Main entry point
18
+ โ”‚ โ”‚
19
+ โ”‚ โ”œโ”€โ”€ client/
20
+ โ”‚ โ”‚ โ”œโ”€โ”€ index.ts # Client exports
21
+ โ”‚ โ”‚ โ”œโ”€โ”€ WhatsAppClient.ts # Main client class
22
+ โ”‚ โ”‚ โ””โ”€โ”€ HttpClient.ts # HTTP request abstraction
23
+ โ”‚ โ”‚
24
+ โ”‚ โ”œโ”€โ”€ schemas/ # Zod schemas (PRIMARY - single source of truth)
25
+ โ”‚ โ”‚ โ”œโ”€โ”€ index.ts # Re-export all schemas
26
+ โ”‚ โ”‚ โ”œโ”€โ”€ common.ts # Shared schemas
27
+ โ”‚ โ”‚ โ”‚
28
+ โ”‚ โ”‚ โ”œโ”€โ”€ messages/ # Message schemas
29
+ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ index.ts
30
+ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ request.ts # Request validation schemas
31
+ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ response.ts # Response validation schemas
32
+ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ message.ts # Message entity schemas
33
+ โ”‚ โ”‚ โ”‚
34
+ โ”‚ โ”‚ โ”œโ”€โ”€ templates/ # Template schemas
35
+ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ index.ts
36
+ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ request.ts
37
+ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ response.ts
38
+ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ template.ts
39
+ โ”‚ โ”‚ โ”‚
40
+ โ”‚ โ”‚ โ””โ”€โ”€ accounts/ # Account schemas
41
+ โ”‚ โ”‚ โ”œโ”€โ”€ index.ts
42
+ โ”‚ โ”‚ โ”œโ”€โ”€ request.ts
43
+ โ”‚ โ”‚ โ”œโ”€โ”€ response.ts
44
+ โ”‚ โ”‚ โ”œโ”€โ”€ waba.ts
45
+ โ”‚ โ”‚ โ””โ”€โ”€ phone-number.ts
46
+ โ”‚ โ”‚
47
+ โ”‚ โ”œโ”€โ”€ types/ # TypeScript types (inferred from schemas)
48
+ โ”‚ โ”‚ โ”œโ”€โ”€ index.ts # Re-export all types
49
+ โ”‚ โ”‚ โ”œโ”€โ”€ client.ts # Client config types (may need explicit types)
50
+ โ”‚ โ”‚ โ”œโ”€โ”€ common.ts # Shared/common types (re-exports from schemas)
51
+ โ”‚ โ”‚ โ”œโ”€โ”€ errors.ts # Error types (inferred from schemas)
52
+ โ”‚ โ”‚ โ”‚
53
+ โ”‚ โ”‚ โ”œโ”€โ”€ messages/ # Message-related types (re-exports)
54
+ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ index.ts
55
+ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ request.ts # Re-export from schemas
56
+ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ response.ts # Re-export from schemas
57
+ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ message.ts # Re-export from schemas
58
+ โ”‚ โ”‚ โ”‚
59
+ โ”‚ โ”‚ โ”œโ”€โ”€ templates/ # Template-related types (re-exports)
60
+ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ index.ts
61
+ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ request.ts
62
+ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ response.ts
63
+ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ template.ts
64
+ โ”‚ โ”‚ โ”‚
65
+ โ”‚ โ”‚ โ””โ”€โ”€ accounts/ # Account-related types (re-exports)
66
+ โ”‚ โ”‚ โ”œโ”€โ”€ index.ts
67
+ โ”‚ โ”‚ โ”œโ”€โ”€ request.ts
68
+ โ”‚ โ”‚ โ”œโ”€โ”€ response.ts
69
+ โ”‚ โ”‚ โ”œโ”€โ”€ waba.ts
70
+ โ”‚ โ”‚ โ””โ”€โ”€ phone-number.ts
71
+ โ”‚ โ”‚
72
+ โ”‚ โ”œโ”€โ”€ services/ # Service implementations
73
+ โ”‚ โ”‚ โ”œโ”€โ”€ index.ts # Service exports
74
+ โ”‚ โ”‚ โ”‚
75
+ โ”‚ โ”‚ โ”œโ”€โ”€ messages/
76
+ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ index.ts
77
+ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ MessagesService.ts # Main service class
78
+ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ methods/ # Individual method implementations
79
+ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ send-text.ts
80
+ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ send-image.ts
81
+ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ send-video.ts
82
+ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ send-audio.ts
83
+ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ send-document.ts
84
+ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ send-location.ts
85
+ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ send-contacts.ts
86
+ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ send-template.ts
87
+ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ send-interactive.ts
88
+ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ send-reaction.ts
89
+ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ mark-as-read.ts
90
+ โ”‚ โ”‚ โ”‚
91
+ โ”‚ โ”‚ โ”œโ”€โ”€ templates/
92
+ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ index.ts
93
+ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ TemplatesService.ts
94
+ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ methods/
95
+ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ create.ts
96
+ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ list.ts
97
+ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ get.ts
98
+ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ update.ts
99
+ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ delete.ts
100
+ โ”‚ โ”‚ โ”‚
101
+ โ”‚ โ”‚ โ””โ”€โ”€ accounts/
102
+ โ”‚ โ”‚ โ”œโ”€โ”€ index.ts
103
+ โ”‚ โ”‚ โ”œโ”€โ”€ AccountsService.ts
104
+ โ”‚ โ”‚ โ”œโ”€โ”€ methods/
105
+ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ get-profile.ts
106
+ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ update-profile.ts
107
+ โ”‚ โ”‚ โ””โ”€โ”€ phone-numbers/
108
+ โ”‚ โ”‚ โ”œโ”€โ”€ index.ts
109
+ โ”‚ โ”‚ โ”œโ”€โ”€ PhoneNumbersService.ts
110
+ โ”‚ โ”‚ โ””โ”€โ”€ methods/
111
+ โ”‚ โ”‚ โ”œโ”€โ”€ list.ts
112
+ โ”‚ โ”‚ โ”œโ”€โ”€ get.ts
113
+ โ”‚ โ”‚ โ””โ”€โ”€ update.ts
114
+ โ”‚ โ”‚
115
+ โ”‚ โ””โ”€โ”€ utils/
116
+ โ”‚ โ”œโ”€โ”€ index.ts
117
+ โ”‚ โ”œโ”€โ”€ errors.ts # Custom error classes
118
+ โ”‚ โ”œโ”€โ”€ validators.ts # Validation helpers
119
+ โ”‚ โ””โ”€โ”€ constants.ts # API constants
120
+ โ”‚
121
+ โ”œโ”€โ”€ tests/ # Test files
122
+ โ”‚ โ”œโ”€โ”€ unit/
123
+ โ”‚ โ”‚ โ”œโ”€โ”€ services/
124
+ โ”‚ โ”‚ โ”œโ”€โ”€ schemas/
125
+ โ”‚ โ”‚ โ””โ”€โ”€ utils/
126
+ โ”‚ โ””โ”€โ”€ integration/
127
+ โ”‚
128
+ โ”œโ”€โ”€ examples/ # Usage examples
129
+ โ”‚ โ”œโ”€โ”€ basic-messaging.ts
130
+ โ”‚ โ”œโ”€โ”€ templates.ts
131
+ โ”‚ โ””โ”€โ”€ accounts.ts
132
+ โ”‚
133
+ โ”œโ”€โ”€ package.json
134
+ โ”œโ”€โ”€ tsconfig.json
135
+ โ”œโ”€โ”€ tsconfig.build.json # Build-specific config
136
+ โ””โ”€โ”€ README.md
137
+ ```
138
+
139
+ ## ๐Ÿ”‘ Key Design Patterns
140
+
141
+ ### 1. Schemas First, Then Types, Then Services
142
+
143
+ **Pattern**: Define Zod schemas โ†’ Infer TypeScript types โ†’ Implement services
144
+
145
+ **Why Schema-First?**
146
+
147
+ - โœ… Single source of truth (schema)
148
+ - โœ… AI-ready (LLMs work with Zod schemas)
149
+ - โœ… Types automatically stay in sync
150
+ - โœ… Less duplication
151
+ - โœ… Modern best practice (tRPC, Next.js, etc.)
152
+
153
+ ```typescript
154
+ // src/schemas/messages/request.ts
155
+ import { z } from "zod";
156
+
157
+ // 1. Define schema first (single source of truth)
158
+ export const sendTextRequestSchema = z.object({
159
+ to: z.string().regex(/^\+[1-9]\d{1,14}$/, "Invalid phone number format"),
160
+ body: z.string().min(1).max(4096),
161
+ previewUrl: z.boolean().optional(),
162
+ phoneNumberId: z.string().optional(),
163
+ });
164
+
165
+ // 2. Infer type from schema
166
+ export type SendTextRequest = z.infer<typeof sendTextRequestSchema>;
167
+
168
+ // src/types/messages/request.ts (optional convenience re-export)
169
+ export type { SendTextRequest } from "../../schemas/messages/request";
170
+
171
+ // src/services/messages/methods/send-text.ts
172
+ import { sendTextRequestSchema } from "../../../schemas/messages/request";
173
+ import type { SendTextRequest } from "../../../schemas/messages/request";
174
+
175
+ export async function sendText(client: HttpClient, request: SendTextRequest) {
176
+ // Schema validates at runtime, type ensures compile-time safety
177
+ const validated = sendTextRequestSchema.parse(request);
178
+ // Implementation...
179
+ }
180
+ ```
181
+
182
+ ### 2. Discriminated Unions for Message Types
183
+
184
+ **Pattern**: Use discriminated unions for type-safe message variants
185
+
186
+ ```typescript
187
+ // src/types/messages/message.ts
188
+ export type TextMessage = {
189
+ type: "text";
190
+ text: {
191
+ body: string;
192
+ previewUrl?: boolean;
193
+ };
194
+ };
195
+
196
+ export type ImageMessage = {
197
+ type: "image";
198
+ image: {
199
+ link?: string;
200
+ id?: string;
201
+ caption?: string;
202
+ };
203
+ };
204
+
205
+ export type VideoMessage = {
206
+ type: "video";
207
+ video: {
208
+ link?: string;
209
+ id?: string;
210
+ caption?: string;
211
+ };
212
+ };
213
+
214
+ export type AudioMessage = {
215
+ type: "audio";
216
+ audio: {
217
+ link?: string;
218
+ id?: string;
219
+ };
220
+ };
221
+
222
+ export type DocumentMessage = {
223
+ type: "document";
224
+ document: {
225
+ link?: string;
226
+ id?: string;
227
+ caption?: string;
228
+ filename?: string;
229
+ };
230
+ };
231
+
232
+ export type LocationMessage = {
233
+ type: "location";
234
+ location: {
235
+ longitude: number;
236
+ latitude: number;
237
+ name?: string;
238
+ address?: string;
239
+ };
240
+ };
241
+
242
+ export type ContactsMessage = {
243
+ type: "contacts";
244
+ contacts: Array<{
245
+ name: {
246
+ formatted_name: string;
247
+ first_name?: string;
248
+ last_name?: string;
249
+ };
250
+ phones?: Array<{ phone: string; type?: string }>;
251
+ emails?: Array<{ email: string; type?: string }>;
252
+ }>;
253
+ };
254
+
255
+ export type TemplateMessage = {
256
+ type: "template";
257
+ template: {
258
+ name: string;
259
+ language: {
260
+ code: string;
261
+ policy?: "deterministic";
262
+ };
263
+ components?: Array<TemplateComponent>;
264
+ };
265
+ };
266
+
267
+ export type InteractiveMessage = {
268
+ type: "interactive";
269
+ interactive: InteractiveContent;
270
+ };
271
+
272
+ export type ReactionMessage = {
273
+ type: "reaction";
274
+ reaction: {
275
+ message_id: string;
276
+ emoji: string;
277
+ };
278
+ };
279
+
280
+ // Discriminated union
281
+ export type MessageContent =
282
+ | TextMessage
283
+ | ImageMessage
284
+ | VideoMessage
285
+ | AudioMessage
286
+ | DocumentMessage
287
+ | LocationMessage
288
+ | ContactsMessage
289
+ | TemplateMessage
290
+ | InteractiveMessage
291
+ | ReactionMessage;
292
+ ```
293
+
294
+ **Corresponding Zod Schema**:
295
+
296
+ ```typescript
297
+ // src/schemas/messages/message.ts
298
+ import { z } from "zod";
299
+
300
+ export const textMessageSchema = z.object({
301
+ type: z.literal("text"),
302
+ text: z.object({
303
+ body: z.string().min(1).max(4096),
304
+ previewUrl: z.boolean().optional(),
305
+ }),
306
+ });
307
+
308
+ export const imageMessageSchema = z.object({
309
+ type: z.literal("image"),
310
+ image: z
311
+ .object({
312
+ link: z.string().url().optional(),
313
+ id: z.string().optional(),
314
+ caption: z.string().max(1024).optional(),
315
+ })
316
+ .refine(
317
+ (data) => data.link || data.id,
318
+ "Either link or id must be provided"
319
+ ),
320
+ });
321
+
322
+ // ... other message schemas
323
+
324
+ export const messageContentSchema = z.discriminatedUnion("type", [
325
+ textMessageSchema,
326
+ imageMessageSchema,
327
+ videoMessageSchema,
328
+ audioMessageSchema,
329
+ documentMessageSchema,
330
+ locationMessageSchema,
331
+ contactsMessageSchema,
332
+ templateMessageSchema,
333
+ interactiveMessageSchema,
334
+ reactionMessageSchema,
335
+ ]);
336
+
337
+ export type MessageContent = z.infer<typeof messageContentSchema>;
338
+ ```
339
+
340
+ ### 3. Service Structure Pattern
341
+
342
+ **Pattern**: Service class with typed methods, each method in separate file
343
+
344
+ ```typescript
345
+ // src/services/messages/MessagesService.ts
346
+ import type { HttpClient } from "../../client/HttpClient";
347
+ import * as sendText from "./methods/send-text";
348
+ import * as sendImage from "./methods/send-image";
349
+ // ... other methods
350
+
351
+ export class MessagesService {
352
+ constructor(private httpClient: HttpClient) {}
353
+
354
+ async sendText(request: sendText.SendTextRequest) {
355
+ return sendText.sendText(this.httpClient, request);
356
+ }
357
+
358
+ async sendImage(request: sendImage.SendImageRequest) {
359
+ return sendImage.sendImage(this.httpClient, request);
360
+ }
361
+
362
+ // ... other methods
363
+ }
364
+ ```
365
+
366
+ ```typescript
367
+ // src/services/messages/methods/send-text.ts
368
+ import type { HttpClient } from "../../../client/HttpClient";
369
+ import { sendTextRequestSchema } from "../../../schemas/messages/request";
370
+ import type { SendTextRequest } from "../../../types/messages/request";
371
+ import type { MessageResponse } from "../../../types/messages/response";
372
+
373
+ export type { SendTextRequest };
374
+
375
+ export async function sendText(
376
+ client: HttpClient,
377
+ request: SendTextRequest
378
+ ): Promise<MessageResponse> {
379
+ const validated = sendTextRequestSchema.parse(request);
380
+
381
+ return client.post<MessageResponse>(
382
+ `/${validated.phoneNumberId || client.phoneNumberId}/messages`,
383
+ {
384
+ messaging_product: "whatsapp",
385
+ recipient_type: "individual",
386
+ to: validated.to,
387
+ type: "text",
388
+ text: {
389
+ body: validated.body,
390
+ preview_url: validated.previewUrl,
391
+ },
392
+ }
393
+ );
394
+ }
395
+ ```
396
+
397
+ ### 4. Client Structure
398
+
399
+ ```typescript
400
+ // src/client/WhatsAppClient.ts
401
+ import type { ClientConfig } from "../types/client";
402
+ import { HttpClient } from "./HttpClient";
403
+ import { MessagesService } from "../services/messages";
404
+ import { TemplatesService } from "../services/templates";
405
+ import { AccountsService } from "../services/accounts";
406
+
407
+ export class WhatsAppClient {
408
+ public readonly messages: MessagesService;
409
+ public readonly templates: TemplatesService;
410
+ public readonly accounts: AccountsService;
411
+
412
+ private readonly httpClient: HttpClient;
413
+
414
+ constructor(config: ClientConfig) {
415
+ this.httpClient = new HttpClient(config);
416
+ this.messages = new MessagesService(this.httpClient);
417
+ this.templates = new TemplatesService(this.httpClient);
418
+ this.accounts = new AccountsService(this.httpClient);
419
+ }
420
+ }
421
+ ```
422
+
423
+ ### 5. HttpClient Pattern
424
+
425
+ ```typescript
426
+ // src/client/HttpClient.ts
427
+ import type { ClientConfig } from "../types/client";
428
+ import type { APIError } from "../types/errors";
429
+
430
+ export class HttpClient {
431
+ private readonly baseURL: string;
432
+ private readonly accessToken: string;
433
+ public readonly phoneNumberId?: string;
434
+ public readonly businessAccountId?: string;
435
+ public readonly apiVersion: string;
436
+
437
+ constructor(config: ClientConfig) {
438
+ this.accessToken = config.accessToken;
439
+ this.phoneNumberId = config.phoneNumberId;
440
+ this.businessAccountId = config.businessAccountId;
441
+ this.apiVersion = config.apiVersion || "v18.0";
442
+ this.baseURL = config.baseURL || "https://graph.facebook.com";
443
+ }
444
+
445
+ async get<T>(path: string, params?: Record<string, unknown>): Promise<T> {
446
+ // Implementation with error handling
447
+ }
448
+
449
+ async post<T>(path: string, body: unknown): Promise<T> {
450
+ // Implementation with error handling
451
+ }
452
+
453
+ async patch<T>(path: string, body: unknown): Promise<T> {
454
+ // Implementation with error handling
455
+ }
456
+
457
+ async delete<T>(path: string): Promise<T> {
458
+ // Implementation with error handling
459
+ }
460
+ }
461
+ ```
462
+
463
+ ## ๐Ÿ“‹ Type & Schema Organization
464
+
465
+ ### Request/Response Pattern
466
+
467
+ For each API endpoint, we define:
468
+
469
+ 1. **Request Type** (`types/*/request.ts`)
470
+ 2. **Request Schema** (`schemas/*/request.ts`)
471
+ 3. **Response Type** (`types/*/response.ts`)
472
+ 4. **Response Schema** (`schemas/*/response.ts`)
473
+
474
+ ### Example: Send Text Message
475
+
476
+ ```typescript
477
+ // schemas/messages/request.ts (PRIMARY - single source of truth)
478
+ import { z } from "zod";
479
+
480
+ export const sendTextRequestSchema = z.object({
481
+ to: z.string().regex(/^\+[1-9]\d{1,14}$/, "Invalid phone number format"),
482
+ body: z.string().min(1).max(4096),
483
+ previewUrl: z.boolean().optional(),
484
+ phoneNumberId: z.string().optional(),
485
+ });
486
+
487
+ // Infer type from schema
488
+ export type SendTextRequest = z.infer<typeof sendTextRequestSchema>;
489
+
490
+ // schemas/messages/response.ts
491
+ export const messageResponseSchema = z.object({
492
+ messaging_product: z.literal("whatsapp"),
493
+ contacts: z.array(
494
+ z.object({
495
+ input: z.string(),
496
+ wa_id: z.string(),
497
+ })
498
+ ),
499
+ messages: z.array(
500
+ z.object({
501
+ id: z.string(),
502
+ })
503
+ ),
504
+ });
505
+
506
+ // Infer type from schema
507
+ export type MessageResponse = z.infer<typeof messageResponseSchema>;
508
+
509
+ // types/messages/request.ts (convenience re-export)
510
+ export type { SendTextRequest } from "../../schemas/messages/request";
511
+
512
+ // types/messages/response.ts (convenience re-export)
513
+ export type { MessageResponse } from "../../schemas/messages/response";
514
+ ```
515
+
516
+ ## ๐ŸŽฏ Index File Pattern
517
+
518
+ Each directory has an `index.ts` that re-exports:
519
+
520
+ ```typescript
521
+ // src/types/messages/index.ts
522
+ export type * from "./request";
523
+ export type * from "./response";
524
+ export type * from "./message";
525
+
526
+ // src/schemas/messages/index.ts
527
+ export * from "./request";
528
+ export * from "./response";
529
+ export * from "./message";
530
+
531
+ // src/services/messages/index.ts
532
+ export { MessagesService } from "./MessagesService";
533
+ export type * from "./methods/send-text";
534
+ export type * from "./methods/send-image";
535
+ // ... etc
536
+
537
+ // src/index.ts
538
+ export { WhatsAppClient } from "./client";
539
+ export type { ClientConfig } from "./types/client";
540
+ export type * from "./types";
541
+ export * from "./schemas";
542
+ ```
543
+
544
+ ## ๐Ÿ”„ Discriminated Union Examples
545
+
546
+ ### Message Types (Request)
547
+
548
+ ```typescript
549
+ // types/messages/request.ts
550
+ export type SendMessageRequest =
551
+ | { type: "text"; to: string; body: string; previewUrl?: boolean }
552
+ | { type: "image"; to: string; imageUrl: string; caption?: string }
553
+ | { type: "video"; to: string; videoUrl: string; caption?: string }
554
+ | {
555
+ type: "template";
556
+ to: string;
557
+ templateName: string;
558
+ language: string;
559
+ parameters?: unknown[];
560
+ };
561
+ // ... etc
562
+ ```
563
+
564
+ ### Template Component Types
565
+
566
+ ```typescript
567
+ // types/templates/template.ts
568
+ export type TemplateComponent =
569
+ | {
570
+ type: "header";
571
+ format: "text" | "image" | "video" | "document";
572
+ text?: string;
573
+ example?: unknown;
574
+ }
575
+ | { type: "body"; text: string; example?: { body_text: unknown[][] } }
576
+ | {
577
+ type: "button";
578
+ sub_type: "quick_reply" | "url" | "phone_number";
579
+ text: string;
580
+ url?: string;
581
+ phone_number?: string;
582
+ };
583
+ ```
584
+
585
+ ### API Error Types
586
+
587
+ ```typescript
588
+ // types/errors.ts
589
+ export type APIError =
590
+ | { code: 131056; type: "rate_limit"; message: string; retry_after?: number }
591
+ | { code: 100; type: "invalid_parameter"; message: string; field?: string }
592
+ | { code: 190; type: "invalid_token"; message: string }
593
+ | {
594
+ code: 80007;
595
+ type: "template_rejected";
596
+ message: string;
597
+ reason?: string;
598
+ };
599
+ // ... etc
600
+ ```
601
+
602
+ ## ๐Ÿงช Testing Structure
603
+
604
+ ```
605
+ tests/
606
+ โ”œโ”€โ”€ unit/
607
+ โ”‚ โ”œโ”€โ”€ schemas/
608
+ โ”‚ โ”‚ โ”œโ”€โ”€ messages.test.ts
609
+ โ”‚ โ”‚ โ”œโ”€โ”€ templates.test.ts
610
+ โ”‚ โ”‚ โ””โ”€โ”€ accounts.test.ts
611
+ โ”‚ โ”œโ”€โ”€ services/
612
+ โ”‚ โ”‚ โ”œโ”€โ”€ messages.test.ts
613
+ โ”‚ โ”‚ โ”œโ”€โ”€ templates.test.ts
614
+ โ”‚ โ”‚ โ””โ”€โ”€ accounts.test.ts
615
+ โ”‚ โ””โ”€โ”€ utils/
616
+ โ”‚ โ””โ”€โ”€ errors.test.ts
617
+ โ””โ”€โ”€ integration/
618
+ โ”œโ”€โ”€ messages.test.ts
619
+ โ”œโ”€โ”€ templates.test.ts
620
+ โ””โ”€โ”€ accounts.test.ts
621
+ ```
622
+
623
+ ## ๐Ÿ“ฆ Package.json Structure
624
+
625
+ ```json
626
+ {
627
+ "name": "whatsapp-cloud",
628
+ "version": "0.1.0",
629
+ "type": "module",
630
+ "main": "./dist/index.js",
631
+ "module": "./dist/index.mjs",
632
+ "types": "./dist/index.d.ts",
633
+ "exports": {
634
+ ".": {
635
+ "types": "./dist/index.d.ts",
636
+ "import": "./dist/index.mjs",
637
+ "require": "./dist/index.js"
638
+ },
639
+ "./schemas": {
640
+ "types": "./dist/schemas/index.d.ts",
641
+ "import": "./dist/schemas/index.mjs",
642
+ "require": "./dist/schemas/index.js"
643
+ }
644
+ },
645
+ "files": ["dist", "README.md"],
646
+ "dependencies": {
647
+ "zod": "^3.23.8"
648
+ },
649
+ "devDependencies": {
650
+ "@types/node": "^20.0.0",
651
+ "tsup": "^8.5.1",
652
+ "typescript": "^5.9.3"
653
+ }
654
+ }
655
+ ```
656
+
657
+ ## ๐Ÿš€ Implementation Order
658
+
659
+ 1. **Foundation**
660
+
661
+ - [ ] Types: `client.ts`, `common.ts`, `errors.ts`
662
+ - [ ] Schemas: `common.ts`
663
+ - [ ] `HttpClient` class
664
+ - [ ] `WhatsAppClient` class (skeleton)
665
+
666
+ 2. **Messages Service**
667
+
668
+ - [ ] Types: `messages/request.ts`, `messages/response.ts`, `messages/message.ts`
669
+ - [ ] Schemas: All message schemas with discriminated unions
670
+ - [ ] Service: `MessagesService` + all send methods
671
+
672
+ 3. **Templates Service**
673
+
674
+ - [ ] Types: `templates/*`
675
+ - [ ] Schemas: `templates/*`
676
+ - [ ] Service: `TemplatesService` + CRUD methods
677
+
678
+ 4. **Accounts Service**
679
+
680
+ - [ ] Types: `accounts/*`
681
+ - [ ] Schemas: `accounts/*`
682
+ - [ ] Service: `AccountsService` + `PhoneNumbersService`
683
+
684
+ 5. **Polish**
685
+ - [ ] Error handling
686
+ - [ ] Documentation
687
+ - [ ] Examples
688
+ - [ ] Tests
689
+
690
+ ## ๐Ÿ’ก Key Benefits
691
+
692
+ 1. **Type Safety**: Full TypeScript coverage, no `any` types
693
+ 2. **AI Ready**: Zod schemas enable LLM tool calling and validation
694
+ 3. **Discriminated Unions**: Type narrowing for better DX
695
+ 4. **Modular**: Easy to extend and maintain
696
+ 5. **Best Practices**: Follows patterns from industry-leading SDKs
697
+ 6. **Tree Shakeable**: Only import what you need
698
+ 7. **Well Organized**: Clear separation of concerns
699
+
700
+ ---
701
+
702
+ This structure ensures:
703
+
704
+ - โœ… **Schemas are the single source of truth** (defined first)
705
+ - โœ… **Types are inferred from schemas** (automatic sync)
706
+ - โœ… **Zod schemas for everything** (AI-ready, LLM function calling)
707
+ - โœ… **Discriminated unions** for type-safe variants
708
+ - โœ… **Clean namespace organization**
709
+ - โœ… **Best-practice package structure** (inspired by Vercel AI SDK & Stripe)
710
+ - โœ… **Easy to extend and maintain**
711
+ - โœ… **Less duplication** (define once, infer type)