create-aws-project 1.2.1

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 (181) hide show
  1. package/README.md +118 -0
  2. package/dist/__tests__/generator/replace-tokens.spec.d.ts +1 -0
  3. package/dist/__tests__/generator/replace-tokens.spec.js +281 -0
  4. package/dist/__tests__/generator.spec.d.ts +1 -0
  5. package/dist/__tests__/generator.spec.js +162 -0
  6. package/dist/__tests__/validation/project-name.spec.d.ts +1 -0
  7. package/dist/__tests__/validation/project-name.spec.js +57 -0
  8. package/dist/__tests__/wizard.spec.d.ts +1 -0
  9. package/dist/__tests__/wizard.spec.js +232 -0
  10. package/dist/aws/iam.d.ts +75 -0
  11. package/dist/aws/iam.js +264 -0
  12. package/dist/aws/organizations.d.ts +79 -0
  13. package/dist/aws/organizations.js +168 -0
  14. package/dist/cli.d.ts +4 -0
  15. package/dist/cli.js +206 -0
  16. package/dist/commands/setup-github.d.ts +4 -0
  17. package/dist/commands/setup-github.js +185 -0
  18. package/dist/generator/copy-file.d.ts +15 -0
  19. package/dist/generator/copy-file.js +56 -0
  20. package/dist/generator/generate-project.d.ts +14 -0
  21. package/dist/generator/generate-project.js +81 -0
  22. package/dist/generator/index.d.ts +4 -0
  23. package/dist/generator/index.js +3 -0
  24. package/dist/generator/replace-tokens.d.ts +29 -0
  25. package/dist/generator/replace-tokens.js +68 -0
  26. package/dist/github/secrets.d.ts +109 -0
  27. package/dist/github/secrets.js +275 -0
  28. package/dist/index.d.ts +2 -0
  29. package/dist/index.js +6 -0
  30. package/dist/prompts/auth.d.ts +3 -0
  31. package/dist/prompts/auth.js +23 -0
  32. package/dist/prompts/aws-config.d.ts +2 -0
  33. package/dist/prompts/aws-config.js +14 -0
  34. package/dist/prompts/features.d.ts +2 -0
  35. package/dist/prompts/features.js +10 -0
  36. package/dist/prompts/github-setup.d.ts +53 -0
  37. package/dist/prompts/github-setup.js +208 -0
  38. package/dist/prompts/org-structure.d.ts +9 -0
  39. package/dist/prompts/org-structure.js +93 -0
  40. package/dist/prompts/platforms.d.ts +2 -0
  41. package/dist/prompts/platforms.js +12 -0
  42. package/dist/prompts/project-name.d.ts +2 -0
  43. package/dist/prompts/project-name.js +8 -0
  44. package/dist/prompts/theme.d.ts +2 -0
  45. package/dist/prompts/theme.js +14 -0
  46. package/dist/templates/index.d.ts +4 -0
  47. package/dist/templates/index.js +2 -0
  48. package/dist/templates/manifest.d.ts +11 -0
  49. package/dist/templates/manifest.js +99 -0
  50. package/dist/templates/tokens.d.ts +39 -0
  51. package/dist/templates/tokens.js +37 -0
  52. package/dist/templates/types.d.ts +52 -0
  53. package/dist/templates/types.js +1 -0
  54. package/dist/types.d.ts +27 -0
  55. package/dist/types.js +1 -0
  56. package/dist/validation/project-name.d.ts +1 -0
  57. package/dist/validation/project-name.js +12 -0
  58. package/dist/wizard.d.ts +2 -0
  59. package/dist/wizard.js +81 -0
  60. package/package.json +68 -0
  61. package/templates/.github/actions/build-and-test/action.yml +24 -0
  62. package/templates/.github/actions/deploy-cdk/action.yml +46 -0
  63. package/templates/.github/actions/deploy-web/action.yml +72 -0
  64. package/templates/.github/actions/setup/action.yml +29 -0
  65. package/templates/.github/pull_request_template.md +15 -0
  66. package/templates/.github/workflows/deploy-dev.yml +80 -0
  67. package/templates/.github/workflows/deploy-prod.yml +67 -0
  68. package/templates/.github/workflows/deploy-stage.yml +77 -0
  69. package/templates/.github/workflows/pull-request.yml +72 -0
  70. package/templates/.vscode/extensions.json +7 -0
  71. package/templates/.vscode/settings.json +67 -0
  72. package/templates/apps/api/.eslintrc.json +18 -0
  73. package/templates/apps/api/cdk/app.ts +93 -0
  74. package/templates/apps/api/cdk/auth/cognito-stack.ts +164 -0
  75. package/templates/apps/api/cdk/cdk.json +73 -0
  76. package/templates/apps/api/cdk/deployment-user-stack.ts +187 -0
  77. package/templates/apps/api/cdk/org-stack.ts +67 -0
  78. package/templates/apps/api/cdk/static-stack.ts +361 -0
  79. package/templates/apps/api/cdk/tsconfig.json +39 -0
  80. package/templates/apps/api/cdk/user-stack.ts +255 -0
  81. package/templates/apps/api/jest.config.ts +38 -0
  82. package/templates/apps/api/lambdas.yml +84 -0
  83. package/templates/apps/api/project.json.template +58 -0
  84. package/templates/apps/api/src/__tests__/setup.ts +10 -0
  85. package/templates/apps/api/src/handlers/users/create-user.ts +52 -0
  86. package/templates/apps/api/src/handlers/users/delete-user.ts +45 -0
  87. package/templates/apps/api/src/handlers/users/get-me.ts +72 -0
  88. package/templates/apps/api/src/handlers/users/get-user.ts +45 -0
  89. package/templates/apps/api/src/handlers/users/get-users.ts +23 -0
  90. package/templates/apps/api/src/handlers/users/index.ts +17 -0
  91. package/templates/apps/api/src/handlers/users/update-user.ts +72 -0
  92. package/templates/apps/api/src/lib/dynamo/dynamo-model.ts +504 -0
  93. package/templates/apps/api/src/lib/dynamo/index.ts +12 -0
  94. package/templates/apps/api/src/lib/dynamo/utils.ts +39 -0
  95. package/templates/apps/api/src/middleware/auth0-auth.ts +97 -0
  96. package/templates/apps/api/src/middleware/cognito-auth.ts +90 -0
  97. package/templates/apps/api/src/models/UserModel.ts +109 -0
  98. package/templates/apps/api/src/schemas/user.schema.ts +44 -0
  99. package/templates/apps/api/src/services/user-service.ts +108 -0
  100. package/templates/apps/api/src/utils/auth-context.ts +60 -0
  101. package/templates/apps/api/src/utils/common/helpers.ts +26 -0
  102. package/templates/apps/api/src/utils/lambda-handler.ts +148 -0
  103. package/templates/apps/api/src/utils/response.ts +52 -0
  104. package/templates/apps/api/src/utils/validator.ts +75 -0
  105. package/templates/apps/api/tsconfig.app.json +15 -0
  106. package/templates/apps/api/tsconfig.json +19 -0
  107. package/templates/apps/api/tsconfig.spec.json +17 -0
  108. package/templates/apps/mobile/.env.example +5 -0
  109. package/templates/apps/mobile/.eslintrc.json +33 -0
  110. package/templates/apps/mobile/app.json +33 -0
  111. package/templates/apps/mobile/assets/.gitkeep +0 -0
  112. package/templates/apps/mobile/babel.config.js +19 -0
  113. package/templates/apps/mobile/index.js +7 -0
  114. package/templates/apps/mobile/jest.config.ts +22 -0
  115. package/templates/apps/mobile/metro.config.js +35 -0
  116. package/templates/apps/mobile/package.json +22 -0
  117. package/templates/apps/mobile/project.json.template +64 -0
  118. package/templates/apps/mobile/src/App.tsx +367 -0
  119. package/templates/apps/mobile/src/__tests__/App.spec.tsx +46 -0
  120. package/templates/apps/mobile/src/__tests__/store/user-store.spec.ts +156 -0
  121. package/templates/apps/mobile/src/config/api.ts +16 -0
  122. package/templates/apps/mobile/src/store/user-store.ts +56 -0
  123. package/templates/apps/mobile/src/test-setup.ts +10 -0
  124. package/templates/apps/mobile/tsconfig.json +22 -0
  125. package/templates/apps/web/.env.example +13 -0
  126. package/templates/apps/web/.eslintrc.json +26 -0
  127. package/templates/apps/web/index.html +13 -0
  128. package/templates/apps/web/jest.config.ts +24 -0
  129. package/templates/apps/web/package.json +15 -0
  130. package/templates/apps/web/project.json.template +66 -0
  131. package/templates/apps/web/src/App.tsx +352 -0
  132. package/templates/apps/web/src/__mocks__/config/api.ts +41 -0
  133. package/templates/apps/web/src/__tests__/App.spec.tsx +240 -0
  134. package/templates/apps/web/src/__tests__/store/user-store.spec.ts +185 -0
  135. package/templates/apps/web/src/auth/auth0-provider.tsx +103 -0
  136. package/templates/apps/web/src/auth/cognito-provider.tsx +143 -0
  137. package/templates/apps/web/src/auth/index.ts +7 -0
  138. package/templates/apps/web/src/auth/use-auth.ts +16 -0
  139. package/templates/apps/web/src/config/amplify-config.ts +31 -0
  140. package/templates/apps/web/src/config/api.ts +38 -0
  141. package/templates/apps/web/src/config/auth0-config.ts +17 -0
  142. package/templates/apps/web/src/main.tsx +41 -0
  143. package/templates/apps/web/src/store/user-store.ts +56 -0
  144. package/templates/apps/web/src/styles.css +165 -0
  145. package/templates/apps/web/src/test-setup.ts +1 -0
  146. package/templates/apps/web/src/theme/index.ts +30 -0
  147. package/templates/apps/web/src/vite-env.d.ts +19 -0
  148. package/templates/apps/web/tsconfig.app.json +24 -0
  149. package/templates/apps/web/tsconfig.json +22 -0
  150. package/templates/apps/web/tsconfig.spec.json +28 -0
  151. package/templates/apps/web/vite.config.ts +87 -0
  152. package/templates/manifest.json +28 -0
  153. package/templates/packages/api-client/.eslintrc.json +18 -0
  154. package/templates/packages/api-client/jest.config.ts +13 -0
  155. package/templates/packages/api-client/package.json +8 -0
  156. package/templates/packages/api-client/project.json.template +34 -0
  157. package/templates/packages/api-client/src/__tests__/api-client.spec.ts +408 -0
  158. package/templates/packages/api-client/src/api-client.ts +201 -0
  159. package/templates/packages/api-client/src/config.ts +193 -0
  160. package/templates/packages/api-client/src/index.ts +9 -0
  161. package/templates/packages/api-client/tsconfig.json +22 -0
  162. package/templates/packages/api-client/tsconfig.lib.json +11 -0
  163. package/templates/packages/api-client/tsconfig.spec.json +14 -0
  164. package/templates/packages/common-types/.eslintrc.json +18 -0
  165. package/templates/packages/common-types/package.json +6 -0
  166. package/templates/packages/common-types/project.json.template +26 -0
  167. package/templates/packages/common-types/src/api.types.ts +24 -0
  168. package/templates/packages/common-types/src/auth.types.ts +36 -0
  169. package/templates/packages/common-types/src/common.types.ts +46 -0
  170. package/templates/packages/common-types/src/index.ts +19 -0
  171. package/templates/packages/common-types/src/lambda.types.ts +39 -0
  172. package/templates/packages/common-types/src/user.types.ts +31 -0
  173. package/templates/packages/common-types/tsconfig.json +19 -0
  174. package/templates/packages/common-types/tsconfig.lib.json +11 -0
  175. package/templates/root/.editorconfig +23 -0
  176. package/templates/root/.nvmrc +1 -0
  177. package/templates/root/eslint.config.js +61 -0
  178. package/templates/root/jest.preset.js +16 -0
  179. package/templates/root/nx.json +29 -0
  180. package/templates/root/package.json +131 -0
  181. package/templates/root/tsconfig.base.json +29 -0
@@ -0,0 +1,408 @@
1
+ import axios from 'axios';
2
+ import { ApiClient, ApiError, createApiClient } from '../api-client';
3
+ import type { User, CreateUserRequest, UpdateUserRequest } from '{{PACKAGE_SCOPE}}/common-types';
4
+
5
+ // Mock axios
6
+ jest.mock('axios');
7
+ const mockedAxios = axios as jest.Mocked<typeof axios>;
8
+
9
+ describe('ApiClient', () => {
10
+ let apiClient: ApiClient;
11
+ const mockAxiosInstance = {
12
+ get: jest.fn(),
13
+ post: jest.fn(),
14
+ put: jest.fn(),
15
+ delete: jest.fn(),
16
+ defaults: {
17
+ headers: {
18
+ common: {} as Record<string, string>,
19
+ },
20
+ baseURL: '',
21
+ },
22
+ interceptors: {
23
+ response: {
24
+ use: jest.fn(),
25
+ },
26
+ },
27
+ };
28
+
29
+ beforeEach(() => {
30
+ jest.clearAllMocks();
31
+ mockedAxios.create.mockReturnValue(mockAxiosInstance as any);
32
+
33
+ apiClient = new ApiClient({
34
+ baseURL: 'https://api.example.com',
35
+ timeout: 5000,
36
+ });
37
+ });
38
+
39
+ describe('constructor', () => {
40
+ it('should create axios instance with correct config', () => {
41
+ expect(mockedAxios.create).toHaveBeenCalledWith({
42
+ baseURL: 'https://api.example.com',
43
+ timeout: 5000,
44
+ headers: {
45
+ 'Content-Type': 'application/json',
46
+ },
47
+ withCredentials: false,
48
+ });
49
+ });
50
+
51
+ it('should create axios instance with custom headers', () => {
52
+ new ApiClient({
53
+ baseURL: 'https://api.example.com',
54
+ headers: {
55
+ 'X-Custom-Header': 'value',
56
+ },
57
+ });
58
+
59
+ expect(mockedAxios.create).toHaveBeenCalledWith(
60
+ expect.objectContaining({
61
+ headers: {
62
+ 'Content-Type': 'application/json',
63
+ 'X-Custom-Header': 'value',
64
+ },
65
+ })
66
+ );
67
+ });
68
+ });
69
+
70
+ describe('createApiClient', () => {
71
+ it('should create and return ApiClient instance', () => {
72
+ const client = createApiClient({ baseURL: 'https://api.example.com' });
73
+ expect(client).toBeInstanceOf(ApiClient);
74
+ });
75
+ });
76
+
77
+ describe('setAuthToken', () => {
78
+ it('should set authorization header', () => {
79
+ apiClient.setAuthToken('test-token');
80
+ expect(mockAxiosInstance.defaults.headers.common['Authorization']).toBe('Bearer test-token');
81
+ });
82
+ });
83
+
84
+ describe('clearAuthToken', () => {
85
+ it('should remove authorization header', () => {
86
+ mockAxiosInstance.defaults.headers.common['Authorization'] = 'Bearer test-token';
87
+ apiClient.clearAuthToken();
88
+ expect(mockAxiosInstance.defaults.headers.common['Authorization']).toBeUndefined();
89
+ });
90
+ });
91
+
92
+ describe('setBaseURL', () => {
93
+ it('should update base URL', () => {
94
+ apiClient.setBaseURL('https://new-api.example.com');
95
+ expect(mockAxiosInstance.defaults.baseURL).toBe('https://new-api.example.com');
96
+ });
97
+ });
98
+
99
+ describe('getUsers', () => {
100
+ it('should fetch all users', async () => {
101
+ const mockUsers: User[] = [
102
+ { id: '1', email: 'user1@example.com', name: 'User 1', createdAt: '2024-01-01' },
103
+ { id: '2', email: 'user2@example.com', name: 'User 2', createdAt: '2024-01-02' },
104
+ ];
105
+
106
+ mockAxiosInstance.get.mockResolvedValue({
107
+ data: {
108
+ success: true,
109
+ data: mockUsers,
110
+ },
111
+ });
112
+
113
+ const users = await apiClient.getUsers();
114
+
115
+ expect(mockAxiosInstance.get).toHaveBeenCalledWith('/api/users', undefined);
116
+ expect(users).toEqual(mockUsers);
117
+ });
118
+
119
+ it('should throw error for invalid response format', async () => {
120
+ mockAxiosInstance.get.mockResolvedValue({
121
+ data: {
122
+ success: false,
123
+ },
124
+ });
125
+
126
+ await expect(apiClient.getUsers()).rejects.toThrow('Invalid response format');
127
+ });
128
+ });
129
+
130
+ describe('getUser', () => {
131
+ it('should fetch user by ID', async () => {
132
+ const mockUser: User = {
133
+ id: '1',
134
+ email: 'user@example.com',
135
+ name: 'Test User',
136
+ createdAt: '2024-01-01',
137
+ };
138
+
139
+ mockAxiosInstance.get.mockResolvedValue({
140
+ data: {
141
+ success: true,
142
+ data: mockUser,
143
+ },
144
+ });
145
+
146
+ const user = await apiClient.getUser('1');
147
+
148
+ expect(mockAxiosInstance.get).toHaveBeenCalledWith('/api/users/1', undefined);
149
+ expect(user).toEqual(mockUser);
150
+ });
151
+
152
+ it('should throw error for invalid response format', async () => {
153
+ mockAxiosInstance.get.mockResolvedValue({
154
+ data: {
155
+ success: false,
156
+ },
157
+ });
158
+
159
+ await expect(apiClient.getUser('1')).rejects.toThrow('Invalid response format');
160
+ });
161
+ });
162
+
163
+ describe('createUser', () => {
164
+ it('should create a new user', async () => {
165
+ const newUser: CreateUserRequest = {
166
+ email: 'new@example.com',
167
+ name: 'New User',
168
+ };
169
+
170
+ const createdUser: User = {
171
+ id: '3',
172
+ ...newUser,
173
+ createdAt: '2024-01-03',
174
+ };
175
+
176
+ mockAxiosInstance.post.mockResolvedValue({
177
+ data: {
178
+ success: true,
179
+ data: createdUser,
180
+ },
181
+ });
182
+
183
+ const user = await apiClient.createUser(newUser);
184
+
185
+ expect(mockAxiosInstance.post).toHaveBeenCalledWith('/api/users', newUser, undefined);
186
+ expect(user).toEqual(createdUser);
187
+ });
188
+
189
+ it('should throw error for invalid response format', async () => {
190
+ mockAxiosInstance.post.mockResolvedValue({
191
+ data: {
192
+ success: false,
193
+ },
194
+ });
195
+
196
+ await expect(
197
+ apiClient.createUser({ email: 'test@example.com', name: 'Test' })
198
+ ).rejects.toThrow('Invalid response format');
199
+ });
200
+ });
201
+
202
+ describe('updateUser', () => {
203
+ it('should update an existing user', async () => {
204
+ const updates: UpdateUserRequest = {
205
+ name: 'Updated Name',
206
+ };
207
+
208
+ const updatedUser: User = {
209
+ id: '1',
210
+ email: 'user@example.com',
211
+ name: 'Updated Name',
212
+ createdAt: '2024-01-01',
213
+ };
214
+
215
+ mockAxiosInstance.put.mockResolvedValue({
216
+ data: {
217
+ success: true,
218
+ data: updatedUser,
219
+ },
220
+ });
221
+
222
+ const user = await apiClient.updateUser('1', updates);
223
+
224
+ expect(mockAxiosInstance.put).toHaveBeenCalledWith('/api/users/1', updates, undefined);
225
+ expect(user).toEqual(updatedUser);
226
+ });
227
+
228
+ it('should throw error for invalid response format', async () => {
229
+ mockAxiosInstance.put.mockResolvedValue({
230
+ data: {
231
+ success: false,
232
+ },
233
+ });
234
+
235
+ await expect(apiClient.updateUser('1', { name: 'Test' })).rejects.toThrow(
236
+ 'Invalid response format'
237
+ );
238
+ });
239
+ });
240
+
241
+ describe('deleteUser', () => {
242
+ it('should delete a user', async () => {
243
+ mockAxiosInstance.delete.mockResolvedValue({
244
+ data: {
245
+ success: true,
246
+ },
247
+ });
248
+
249
+ await apiClient.deleteUser('1');
250
+
251
+ expect(mockAxiosInstance.delete).toHaveBeenCalledWith('/api/users/1', undefined);
252
+ });
253
+ });
254
+
255
+ describe('getAxiosInstance', () => {
256
+ it('should return the axios instance', () => {
257
+ const instance = apiClient.getAxiosInstance();
258
+ expect(instance).toBe(mockAxiosInstance);
259
+ });
260
+ });
261
+ });
262
+
263
+ describe('Response Interceptor Error Handling', () => {
264
+ let apiClient: ApiClient;
265
+ let successHandler: (response: any) => any;
266
+ let errorHandler: (error: any) => never;
267
+
268
+ beforeEach(() => {
269
+ jest.clearAllMocks();
270
+
271
+ // Capture the handlers when interceptors.response.use is called
272
+ const mockAxiosInstance = {
273
+ get: jest.fn(),
274
+ post: jest.fn(),
275
+ put: jest.fn(),
276
+ delete: jest.fn(),
277
+ defaults: {
278
+ headers: {
279
+ common: {} as Record<string, string>,
280
+ },
281
+ baseURL: '',
282
+ },
283
+ interceptors: {
284
+ response: {
285
+ use: jest.fn((sucHandler, errHandler) => {
286
+ successHandler = sucHandler;
287
+ errorHandler = errHandler;
288
+ }),
289
+ },
290
+ },
291
+ };
292
+
293
+ (axios as jest.Mocked<typeof axios>).create.mockReturnValue(mockAxiosInstance as any);
294
+ apiClient = new ApiClient({ baseURL: 'https://api.example.com' });
295
+ });
296
+
297
+ it('should pass through successful responses', () => {
298
+ const mockResponse = { data: { success: true }, status: 200 };
299
+ const result = successHandler(mockResponse);
300
+ expect(result).toBe(mockResponse);
301
+ });
302
+
303
+ it('should handle server error with API error format', () => {
304
+ const axiosError = {
305
+ response: {
306
+ status: 400,
307
+ data: {
308
+ error: {
309
+ message: 'Validation failed',
310
+ code: 'VALIDATION_ERROR',
311
+ details: { field: 'email' },
312
+ },
313
+ },
314
+ },
315
+ message: 'Request failed',
316
+ };
317
+
318
+ expect(() => errorHandler(axiosError)).toThrow(ApiError);
319
+ try {
320
+ errorHandler(axiosError);
321
+ } catch (e) {
322
+ const error = e as ApiError;
323
+ expect(error.message).toBe('Validation failed');
324
+ expect(error.statusCode).toBe(400);
325
+ expect(error.code).toBe('VALIDATION_ERROR');
326
+ expect(error.details).toEqual({ field: 'email' });
327
+ }
328
+ });
329
+
330
+ it('should handle server error without API error format', () => {
331
+ const axiosError = {
332
+ response: {
333
+ status: 500,
334
+ data: { message: 'Internal server error' },
335
+ },
336
+ message: 'Server error',
337
+ };
338
+
339
+ expect(() => errorHandler(axiosError)).toThrow(ApiError);
340
+ try {
341
+ errorHandler(axiosError);
342
+ } catch (e) {
343
+ const error = e as ApiError;
344
+ expect(error.message).toBe('Server error');
345
+ expect(error.statusCode).toBe(500);
346
+ expect(error.code).toBe('API_ERROR');
347
+ }
348
+ });
349
+
350
+ it('should handle network error (no response)', () => {
351
+ const axiosError = {
352
+ request: {},
353
+ message: 'Network Error',
354
+ };
355
+
356
+ expect(() => errorHandler(axiosError)).toThrow(ApiError);
357
+ try {
358
+ errorHandler(axiosError);
359
+ } catch (e) {
360
+ const error = e as ApiError;
361
+ expect(error.message).toBe('No response from server');
362
+ expect(error.statusCode).toBeUndefined();
363
+ expect(error.code).toBe('NETWORK_ERROR');
364
+ }
365
+ });
366
+
367
+ it('should handle request setup error', () => {
368
+ const axiosError = {
369
+ message: 'Invalid URL',
370
+ };
371
+
372
+ expect(() => errorHandler(axiosError)).toThrow(ApiError);
373
+ try {
374
+ errorHandler(axiosError);
375
+ } catch (e) {
376
+ const error = e as ApiError;
377
+ expect(error.message).toBe('Invalid URL');
378
+ expect(error.statusCode).toBeUndefined();
379
+ expect(error.code).toBe('REQUEST_ERROR');
380
+ }
381
+ });
382
+ });
383
+
384
+ describe('ApiError', () => {
385
+ it('should create error with all properties', () => {
386
+ const error = new ApiError(
387
+ 'Test error',
388
+ 400,
389
+ 'TEST_ERROR',
390
+ { field: 'email' }
391
+ );
392
+
393
+ expect(error.message).toBe('Test error');
394
+ expect(error.statusCode).toBe(400);
395
+ expect(error.code).toBe('TEST_ERROR');
396
+ expect(error.details).toEqual({ field: 'email' });
397
+ expect(error.name).toBe('ApiError');
398
+ });
399
+
400
+ it('should create error with minimal properties', () => {
401
+ const error = new ApiError('Simple error');
402
+
403
+ expect(error.message).toBe('Simple error');
404
+ expect(error.statusCode).toBeUndefined();
405
+ expect(error.code).toBeUndefined();
406
+ expect(error.details).toBeUndefined();
407
+ });
408
+ });
@@ -0,0 +1,201 @@
1
+ import axios, { type AxiosInstance, type AxiosRequestConfig, AxiosError } from 'axios';
2
+ import type {
3
+ User,
4
+ CreateUserRequest,
5
+ UpdateUserRequest,
6
+ ApiResponse,
7
+ ApiError as ApiErrorType,
8
+ } from '{{PACKAGE_SCOPE}}/common-types';
9
+
10
+ /**
11
+ * Configuration options for the API client
12
+ */
13
+ export interface ApiClientConfig {
14
+ /**
15
+ * Base URL for the API (e.g., 'https://api.example.com' or 'http://localhost:3000')
16
+ */
17
+ baseURL: string;
18
+
19
+ /**
20
+ * Request timeout in milliseconds (default: 30000)
21
+ */
22
+ timeout?: number;
23
+
24
+ /**
25
+ * Additional headers to include in all requests
26
+ */
27
+ headers?: Record<string, string>;
28
+
29
+ /**
30
+ * Whether to send cookies with requests (default: false)
31
+ */
32
+ withCredentials?: boolean;
33
+ }
34
+
35
+ /**
36
+ * Custom error class for API errors
37
+ */
38
+ export class ApiError extends Error {
39
+ public readonly statusCode?: number;
40
+ public readonly code?: string;
41
+ public readonly details?: unknown;
42
+
43
+ constructor(message: string, statusCode?: number, code?: string, details?: unknown) {
44
+ super(message);
45
+ this.name = 'ApiError';
46
+ this.statusCode = statusCode;
47
+ this.code = code;
48
+ this.details = details;
49
+
50
+ // Restore prototype chain for instanceof checks
51
+ Object.setPrototypeOf(this, ApiError.prototype);
52
+ }
53
+ }
54
+
55
+ /**
56
+ * API Client for the monorepo
57
+ *
58
+ * Provides type-safe methods for interacting with the backend API.
59
+ */
60
+ export class ApiClient {
61
+ private client: AxiosInstance;
62
+
63
+ constructor(config: ApiClientConfig) {
64
+ this.client = axios.create({
65
+ baseURL: config.baseURL,
66
+ timeout: config.timeout || 30000,
67
+ headers: {
68
+ 'Content-Type': 'application/json',
69
+ ...config.headers,
70
+ },
71
+ withCredentials: config.withCredentials || false,
72
+ });
73
+
74
+ // Add response interceptor for error handling
75
+ this.client.interceptors.response.use(
76
+ (response) => response,
77
+ (error: AxiosError) => {
78
+ if (error.response) {
79
+ // Server responded with error status
80
+ const data = error.response.data as ApiResponse<unknown> | ApiErrorType | undefined;
81
+
82
+ if (data && typeof data === 'object' && 'error' in data) {
83
+ const apiError = data.error as ApiErrorType;
84
+ throw new ApiError(
85
+ apiError.message,
86
+ error.response.status,
87
+ apiError.code,
88
+ apiError.details
89
+ );
90
+ }
91
+
92
+ throw new ApiError(
93
+ error.message,
94
+ error.response.status,
95
+ 'API_ERROR',
96
+ error.response.data
97
+ );
98
+ } else if (error.request) {
99
+ // Request made but no response received
100
+ throw new ApiError('No response from server', undefined, 'NETWORK_ERROR');
101
+ } else {
102
+ // Something else happened
103
+ throw new ApiError(error.message, undefined, 'REQUEST_ERROR');
104
+ }
105
+ }
106
+ );
107
+ }
108
+
109
+ /**
110
+ * Set authentication token
111
+ */
112
+ public setAuthToken(token: string): void {
113
+ this.client.defaults.headers.common['Authorization'] = `Bearer ${token}`;
114
+ }
115
+
116
+ /**
117
+ * Clear authentication token
118
+ */
119
+ public clearAuthToken(): void {
120
+ delete this.client.defaults.headers.common['Authorization'];
121
+ }
122
+
123
+ /**
124
+ * Update the base URL
125
+ */
126
+ public setBaseURL(baseURL: string): void {
127
+ this.client.defaults.baseURL = baseURL;
128
+ }
129
+
130
+ /**
131
+ * Get all users
132
+ */
133
+ public async getUsers(config?: AxiosRequestConfig): Promise<User[]> {
134
+ const response = await this.client.get<ApiResponse<User[]>>('/api/users', config);
135
+ if (response.data.success && response.data.data) {
136
+ return response.data.data;
137
+ }
138
+ throw new ApiError('Invalid response format');
139
+ }
140
+
141
+ /**
142
+ * Get user by ID
143
+ */
144
+ public async getUser(id: string, config?: AxiosRequestConfig): Promise<User> {
145
+ const response = await this.client.get<ApiResponse<User>>(`/api/users/${id}`, config);
146
+ if (response.data.success && response.data.data) {
147
+ return response.data.data;
148
+ }
149
+ throw new ApiError('Invalid response format');
150
+ }
151
+
152
+ /**
153
+ * Create a new user
154
+ */
155
+ public async createUser(
156
+ data: CreateUserRequest,
157
+ config?: AxiosRequestConfig
158
+ ): Promise<User> {
159
+ const response = await this.client.post<ApiResponse<User>>('/api/users', data, config);
160
+ if (response.data.success && response.data.data) {
161
+ return response.data.data;
162
+ }
163
+ throw new ApiError('Invalid response format');
164
+ }
165
+
166
+ /**
167
+ * Update an existing user
168
+ */
169
+ public async updateUser(
170
+ id: string,
171
+ data: UpdateUserRequest,
172
+ config?: AxiosRequestConfig
173
+ ): Promise<User> {
174
+ const response = await this.client.put<ApiResponse<User>>(`/api/users/${id}`, data, config);
175
+ if (response.data.success && response.data.data) {
176
+ return response.data.data;
177
+ }
178
+ throw new ApiError('Invalid response format');
179
+ }
180
+
181
+ /**
182
+ * Delete a user
183
+ */
184
+ public async deleteUser(id: string, config?: AxiosRequestConfig): Promise<void> {
185
+ await this.client.delete(`/api/users/${id}`, config);
186
+ }
187
+
188
+ /**
189
+ * Get the underlying axios instance for advanced usage
190
+ */
191
+ public getAxiosInstance(): AxiosInstance {
192
+ return this.client;
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Factory function to create an API client
198
+ */
199
+ export function createApiClient(config: ApiClientConfig): ApiClient {
200
+ return new ApiClient(config);
201
+ }