@next-feature/client 0.1.0-beta → 0.1.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.
package/.babelrc CHANGED
@@ -1,12 +1,12 @@
1
- {
2
- "presets": [
3
- [
4
- "@nx/react/babel",
5
- {
6
- "runtime": "automatic",
7
- "useBuiltIns": "usage"
8
- }
9
- ]
10
- ],
11
- "plugins": []
12
- }
1
+ {
2
+ "presets": [
3
+ [
4
+ "@nx/react/babel",
5
+ {
6
+ "runtime": "automatic",
7
+ "useBuiltIns": "usage"
8
+ }
9
+ ]
10
+ ],
11
+ "plugins": []
12
+ }
package/README.md CHANGED
@@ -1,518 +1,7 @@
1
- # @next-feature/client v0.1.0
2
-
3
- Complete API client library with built-in error handling, validation support, and React utilities for Next.js applications.
4
-
5
- ## Overview
6
-
7
- **@next-feature/client** provides a production-ready API client with:
8
-
9
- - **ApiClient** - Axios wrapper with JWT/refresh token handling and retry logic
10
- - **ApiError** - Custom error class with status helpers and validation support
11
- - **Error Utilities** - Formatting, extraction, and handling functions
12
- - **React Hooks** - `useApiError` for error state management
13
- - **Error Boundary** - React component for error handling
14
- - **Type Definitions** - Full TypeScript support
15
-
16
- ## Quick Start
17
-
18
- ### Installation
19
-
20
- ```bash
21
- npm install @next-feature/client
22
- ```
23
-
24
- ### Basic Usage
25
-
26
- ```typescript
27
- import { ApiClient, ApiError, type ApiResponse } from '@next-feature/client';
28
-
29
- // Create client
30
- const apiClient = new ApiClient({
31
- baseURL: process.env.NEXT_PUBLIC_API_URL
32
- });
33
-
34
- // Use in server actions
35
- export async function getUser(id: string): Promise<ApiResponse<User>> {
36
- try {
37
- const user = await apiClient.get<User>(`/users/${id}`);
38
- return { success: true, data: user };
39
- } catch (error) {
40
- const apiError = ApiError.of(error);
41
- return {
42
- success: false,
43
- message: apiError.message,
44
- error: apiError.problemDetail
45
- };
46
- }
47
- }
48
- ```
49
-
50
- ## Features
51
-
52
- ### 🔐 Token Management
53
-
54
- Automatic JWT/refresh token handling with pending request queue:
55
-
56
- ```typescript
57
- const apiClient = new ApiClient({
58
- baseURL: 'https://api.example.com',
59
- enableRefreshToken: true,
60
- onRefreshToken: async () => {
61
- const response = await fetch('/api/refresh');
62
- return (await response.json()).token;
63
- },
64
- onUnauthorized: async () => {
65
- // Redirect to login
66
- window.location.href = '/login';
67
- }
68
- });
69
- ```
70
-
71
- ### 🔄 Retry Logic
72
-
73
- Automatic retry with exponential backoff:
74
-
75
- ```typescript
76
- const apiClient = new ApiClient({
77
- baseURL: 'https://api.example.com',
78
- maxRetries: 3, // Retry up to 3 times
79
- retryDelay: 1000 // Start with 1 second delay
80
- });
81
-
82
- // Automatically retries on:
83
- // - Network errors
84
- // - 5xx server errors
85
- // - 429 (Too Many Requests)
86
- ```
87
-
88
- ### 🎯 Error Handling
89
-
90
- Custom `ApiError` class with helpers:
91
-
92
- ```typescript
93
- import { ApiError } from '@next-feature/client';
94
-
95
- try {
96
- await apiClient.get('/users');
97
- } catch (error) {
98
- const apiError = ApiError.of(error);
99
-
100
- if (apiError.isUnauthorized) {
101
- // Handle 401 Unauthorized
102
- } else if (apiError.isServerError) {
103
- // Handle 5xx
104
- } else if (apiError.isNotFound) {
105
- // Handle 404
106
- }
107
-
108
- console.error(apiError.message);
109
- console.error(apiError.problemDetail);
110
- }
111
- ```
112
-
113
- ### ✓ Zod Validation Errors
114
-
115
- Convert form validation errors automatically:
116
-
117
- ```typescript
118
- import { z } from 'zod';
119
- import { ApiError } from '@next-feature/client';
120
-
121
- const schema = z.object({
122
- email: z.string().email(),
123
- password: z.string().min(8)
124
- });
125
-
126
- const result = schema.safeParse(formData);
127
-
128
- if (!result.success) {
129
- // Convert Zod errors to ApiError with validation details
130
- const apiError = ApiError.fromZodError(result.error);
131
-
132
- // apiError.problemDetail.errors:
133
- // { 'email': ['Invalid email'], 'password': ['Too short'] }
134
- }
135
- ```
136
-
137
- ### 🎨 Spring Boot Integration
138
-
139
- Compatible with Spring Boot `ProblemDetail` error responses:
140
-
141
- ```typescript
142
- // Backend returns:
143
- {
144
- "type": "https://example.com/probs/invalid-request",
145
- "title": "Invalid Request",
146
- "status": 400,
147
- "detail": "The request contained invalid data",
148
- "errors": {
149
- "email": ["Invalid email format"]
150
- }
151
- }
152
-
153
- // Automatically parsed into:
154
- const apiError = ApiError.of(error);
155
- console.log(apiError.problemDetail.errors); // { email: ['Invalid...'] }
156
- ```
157
-
158
- ## HTTP Methods
159
-
160
- ```typescript
161
- // GET
162
- const data = await apiClient.get<T>('/endpoint');
163
-
164
- // POST
165
- const data = await apiClient.post<T>('/endpoint', { body });
166
-
167
- // PUT
168
- const data = await apiClient.put<T>('/endpoint', { body });
169
-
170
- // PATCH
171
- const data = await apiClient.patch<T>('/endpoint', { body });
172
-
173
- // DELETE
174
- const data = await apiClient.delete<T>('/endpoint');
175
- ```
176
-
177
- ## Interceptors
178
-
179
- ```typescript
180
- const apiClient = new ApiClient({ baseURL: '...' });
181
-
182
- // Request interceptor
183
- apiClient.getAxiosInstance().interceptors.request.use((config) => {
184
- // Add custom headers
185
- config.headers['X-Custom-Header'] = 'value';
186
- return config;
187
- });
188
-
189
- // Response interceptor
190
- apiClient.getAxiosInstance().interceptors.response.use(
191
- (response) => response,
192
- (error) => {
193
- // Global error handling
194
- console.error(error);
195
- return Promise.reject(error);
196
- }
197
- );
198
- ```
199
-
200
- ## Error Utilities
201
-
202
- ### getErrorMessage()
203
-
204
- Extract user-friendly error message:
205
-
206
- ```typescript
207
- import { getErrorMessage } from '@next-feature/client';
208
-
209
- const message = getErrorMessage(error);
210
- toast.error(message);
211
- ```
212
-
213
- ### formatProblemDetail()
214
-
215
- Format error for display:
216
-
217
- ```typescript
218
- import { formatProblemDetail } from '@next-feature/client';
219
-
220
- const formatted = formatProblemDetail(apiError);
221
- console.log(formatted);
222
- ```
223
-
224
- ### isHttpStatus()
225
-
226
- Check specific HTTP status:
227
-
228
- ```typescript
229
- import { isHttpStatus } from '@next-feature/client';
230
-
231
- if (isHttpStatus(error, 401)) {
232
- redirectToLogin();
233
- }
234
- ```
235
-
236
- ### handleApiError()
237
-
238
- Global error handler:
239
-
240
- ```typescript
241
- import { handleApiError } from '@next-feature/client';
242
-
243
- try {
244
- await apiClient.get('/data');
245
- } catch (error) {
246
- handleApiError(error); // Logs appropriate messages
247
- }
248
- ```
249
-
250
- ### extractValidationErrors()
251
-
252
- Extract form field errors:
253
-
254
- ```typescript
255
- import { extractValidationErrors } from '@next-feature/client';
256
-
257
- const fieldErrors = extractValidationErrors(apiError);
258
- // { email: ['Invalid email'], password: ['Too short'] }
259
-
260
- // Use with forms
261
- Object.entries(fieldErrors).forEach(([field, messages]) => {
262
- showFieldError(field, messages[0]);
263
- });
264
- ```
265
-
266
- ## React Integration
267
-
268
- ### useApiError Hook
269
-
270
- Manage API error state in components:
271
-
272
- ```typescript
273
- 'use client';
274
-
275
- import { useApiError } from '@next-feature/client';
276
-
277
- export function MyComponent() {
278
- const { error, setError, clearError } = useApiError();
279
-
280
- const handleSubmit = async (data) => {
281
- try {
282
- const result = await myAction(data);
283
- if (!result.success) {
284
- setError(result.error);
285
- }
286
- } catch (err) {
287
- setError(ApiError.of(err));
288
- }
289
- };
290
-
291
- return (
292
- <>
293
- {error && (
294
- <div className="error-alert">
295
- <p>{error.message}</p>
296
- <button onClick={clearError}>Dismiss</button>
297
- </div>
298
- )}
299
- {/* Form JSX */}
300
- </>
301
- );
302
- }
303
- ```
304
-
305
- ### ApiErrorBoundary Component
306
-
307
- Error boundary for API errors:
308
-
309
- ```typescript
310
- import { ApiErrorBoundary } from '@next-feature/client';
311
-
312
- export function App() {
313
- return (
314
- <ApiErrorBoundary
315
- fallback={(error, reset) => (
316
- <div className="error-page">
317
- <h1>Something went wrong</h1>
318
- <p>{error.message}</p>
319
- <button onClick={reset}>Try again</button>
320
- </div>
321
- )}
322
- >
323
- <YourContent />
324
- </ApiErrorBoundary>
325
- );
326
- }
327
- ```
328
-
329
- ## Type Definitions
330
-
331
- ### ApiResponse<T>
332
-
333
- Standard API response envelope:
334
-
335
- ```typescript
336
- interface ApiResponse<T> {
337
- success: boolean;
338
- data?: T;
339
- error?: ProblemDetail | null;
340
- message?: string;
341
- }
342
- ```
343
-
344
- ### ProblemDetail
345
-
346
- Error detail structure:
347
-
348
- ```typescript
349
- interface ProblemDetail {
350
- type: string;
351
- title: string;
352
- status: number;
353
- detail?: string;
354
- instance?: string;
355
- errors?: Record<string, unknown>;
356
- [key: string]: unknown;
357
- }
358
- ```
359
-
360
- ### ApiClientConfig
361
-
362
- Client configuration:
363
-
364
- ```typescript
365
- interface ApiClientConfig {
366
- baseURL: string;
367
- timeout?: number; // Default: 30000ms
368
- enableRefreshToken?: boolean; // Default: true
369
- maxRetries?: number; // Default: 3
370
- retryDelay?: number; // Default: 1000ms
371
- onAuthenticated?: (config) => void | Promise<void>;
372
- onUnauthorized?: () => void | Promise<void>;
373
- onRefreshTokenExpired?: () => void | Promise<void>;
374
- onRefreshToken?: () => string | Promise<string>;
375
- }
376
- ```
377
-
378
- ## Integration with next-feature
379
-
380
- This library is designed to work seamlessly with the **next-feature generator plugin**:
381
-
382
- ```bash
383
- # Actions automatically import from this library
384
- npx nx g next-feature:action --name=getUser --projectName=myapp
385
-
386
- # Generated action will use:
387
- import { ApiError, type ApiResponse } from '@next-feature/client';
388
- ```
389
-
390
- Generated client configurations use this library:
391
-
392
- ```bash
393
- # Client-config generator creates lib/client/config.ts
394
- npx nx g next-feature:client-config --projectName=myapp
395
-
396
- # Which imports and uses:
397
- import { ApiClient, ApiError } from '@next-feature/client';
398
- ```
399
-
400
- ## Testing
401
-
402
- ```bash
403
- npm test
404
- ```
405
-
406
- Run tests with coverage:
407
-
408
- ```bash
409
- npm test -- --coverage
410
- ```
411
-
412
- ## Development
413
-
414
- ### Build
415
-
416
- ```bash
417
- npm run build
418
- ```
419
-
420
- ### Lint
421
-
422
- ```bash
423
- npm run lint
424
- ```
425
-
426
- ### Format
427
-
428
- ```bash
429
- npm run format
430
- ```
431
-
432
- ## Examples
433
-
434
- ### Form Submission with Validation
435
-
436
- ```typescript
437
- 'use server';
438
-
439
- import { z } from 'zod';
440
- import { ApiError, type ApiResponse } from '@next-feature/client';
441
- import apiClient from './config';
442
-
443
- const schema = z.object({
444
- email: z.string().email(),
445
- password: z.string().min(8)
446
- });
447
-
448
- export async function loginAction(
449
- formData: FormData
450
- ): Promise<ApiResponse<{ token: string }>> {
451
- const email = formData.get('email');
452
- const password = formData.get('password');
453
-
454
- const result = schema.safeParse({ email, password });
455
-
456
- if (!result.success) {
457
- const apiError = ApiError.fromZodError(result.error);
458
- return {
459
- success: false,
460
- message: apiError.message,
461
- error: apiError.problemDetail
462
- };
463
- }
464
-
465
- try {
466
- const token = await apiClient.post<{ token: string }>('/auth/login', result.data);
467
- return { success: true, data: token };
468
- } catch (error) {
469
- const apiError = ApiError.of(error);
470
- return {
471
- success: false,
472
- message: apiError.message,
473
- error: apiError.problemDetail
474
- };
475
- }
476
- }
477
- ```
478
-
479
- ### API Data Fetching
480
-
481
- ```typescript
482
- export async function getProduct(id: string): Promise<ApiResponse<Product>> {
483
- try {
484
- const product = await apiClient.get<Product>(`/products/${id}`);
485
- return { success: true, data: product };
486
- } catch (error) {
487
- const apiError = ApiError.of(error);
488
- return {
489
- success: false,
490
- message: `Failed to fetch product: ${apiError.message}`,
491
- error: apiError.problemDetail
492
- };
493
- }
494
- }
495
- ```
496
-
497
- ## Troubleshooting
498
-
499
- ### Request not retried
500
-
501
- Check `enableRefreshToken` and `maxRetries` configuration.
502
-
503
- ### Token not refreshed
504
-
505
- Ensure `onRefreshToken` is implemented and returns valid token string.
506
-
507
- ### Type mismatches
508
-
509
- Ensure your `ProblemDetail` shape matches your backend error responses.
510
-
511
- ## License
512
-
513
- MIT
514
-
515
- ## See Also
516
-
517
- - [next-feature Plugin](../../next-feature/README.md) - Generator plugin using this library
518
- - [NextFeature](../../README.md) - Complete ecosystem overview
1
+ # client
2
+
3
+ This library was generated with [Nx](https://nx.dev).
4
+
5
+ ## Running unit tests
6
+
7
+ Run `nx test client` to execute the unit tests via [Vitest](https://vitest.dev/).
package/eslint.config.mjs CHANGED
@@ -1,12 +1,12 @@
1
- import nx from '@nx/eslint-plugin';
2
- import baseConfig from '../../eslint.config.mjs';
3
-
4
- export default [
5
- ...baseConfig,
6
- ...nx.configs['flat/react'],
7
- {
8
- files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
9
- // Override or add rules here
10
- rules: {},
11
- },
12
- ];
1
+ import nx from '@nx/eslint-plugin';
2
+ import baseConfig from '../../eslint.config.mjs';
3
+
4
+ export default [
5
+ ...baseConfig,
6
+ ...nx.configs['flat/react'],
7
+ {
8
+ files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
9
+ // Override or add rules here
10
+ rules: {},
11
+ },
12
+ ];
@@ -1,10 +1,10 @@
1
- export default {
2
- displayName: 'client',
3
- preset: '../../jest.preset.js',
4
- transform: {
5
- '^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nx/react/plugins/jest',
6
- '^.+\\.[tj]sx?$': ['babel-jest', { presets: ['@nx/react/babel'] }],
7
- },
8
- moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
9
- coverageDirectory: '../../coverage/clients/client',
10
- };
1
+ module.exports = {
2
+ displayName: 'client',
3
+ preset: '../../jest.preset.js',
4
+ transform: {
5
+ '^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nx/react/plugins/jest',
6
+ '^.+\\.[tj]sx?$': ['babel-jest', { presets: ['@nx/react/babel'] }],
7
+ },
8
+ moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
9
+ coverageDirectory: '../../coverage/clients/client',
10
+ };
package/package.json CHANGED
@@ -1,15 +1,17 @@
1
1
  {
2
2
  "name": "@next-feature/client",
3
- "version": "0.1.0-beta",
3
+ "version": "0.1.1",
4
4
  "main": "./index.js",
5
5
  "types": "./index.d.ts",
6
6
  "exports": {
7
7
  ".": {
8
8
  "import": "./index.mjs",
9
9
  "require": "./index.js"
10
+ },
11
+ "./server": {
12
+ "types": "./dist/server.d.ts",
13
+ "import": "./dist/server.js",
14
+ "default": "./dist/server.js"
10
15
  }
11
- },
12
- "publishConfig": {
13
- "access": "public"
14
16
  }
15
17
  }
package/project.json CHANGED
@@ -1,32 +1,32 @@
1
- {
2
- "name": "client",
3
- "$schema": "../../node_modules/nx/schemas/project-schema.json",
4
- "sourceRoot": "clients/client/src",
5
- "projectType": "library",
6
- "tags": [],
7
- "targets": {
8
- "build": {
9
- "executor": "@nx/vite:build",
10
- "outputs": ["{options.outputPath}"],
11
- "defaultConfiguration": "production",
12
- "options": {
13
- "outputPath": "dist/clients/client"
14
- },
15
- "configurations": {
16
- "development": {
17
- "mode": "development"
18
- },
19
- "production": {
20
- "mode": "production"
21
- }
22
- }
23
- },
24
- "test": {
25
- "executor": "@nx/jest:jest",
26
- "outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
27
- "options": {
28
- "jestConfig": "clients/client/jest.config.ts"
29
- }
30
- }
31
- }
32
- }
1
+ {
2
+ "name": "client",
3
+ "$schema": "../../node_modules/nx/schemas/project-schema.json",
4
+ "sourceRoot": "clients/client/src",
5
+ "projectType": "library",
6
+ "tags": [],
7
+ "targets": {
8
+ "build": {
9
+ "executor": "@nx/vite:build",
10
+ "outputs": ["{options.outputPath}"],
11
+ "defaultConfiguration": "production",
12
+ "options": {
13
+ "outputPath": "dist/clients/client"
14
+ },
15
+ "configurations": {
16
+ "development": {
17
+ "mode": "development"
18
+ },
19
+ "production": {
20
+ "mode": "production"
21
+ }
22
+ }
23
+ },
24
+ "test": {
25
+ "executor": "@nx/jest:jest",
26
+ "outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
27
+ "options": {
28
+ "jestConfig": "clients/client/jest.config.cts"
29
+ }
30
+ }
31
+ }
32
+ }