UNPKG

@appello/services

Version:

Services package with api / graphql

602 lines (462 loc) 18.1 kB
# Frontend Services Library A comprehensive TypeScript library providing reusable service utilities for handling API requests, GraphQL operations, and data fetching across web and mobile React applications. ## 📋 Table of Contents - [✨ Features](#features) - [📦 Installation](#installation) - [🚀 Quick Start](#quick-start) - [🏗️ Core Services](#core-services) - [🔗 Integration Examples](#integration-examples) - [🛠️ Utilities](#utilities) - [⚙️ Configuration](#configuration) - [💡 Complete Example](#complete-example) - [❓ Troubleshooting](#troubleshooting) ## <a id="features"></a>✨ Features - 🌐 **REST API Service** - Axios-based with automatic token refresh - 🔗 **GraphQL Client** - Apollo Client with auth, error handling, and subscriptions - 🏪 **RTK Query Integration** - Redux Toolkit Query base query - 🔄 **React Query Support** - TanStack React Query client factory - 🔐 **Authentication** - JWT token management with refresh logic - ⚠️ **Error Handling** - Standardized error processing for forms - 🎯 **TypeScript** - Full type safety and IntelliSense support - 📱 **Cross-Platform** - Works with React web and React Native ## <a id="installation"></a>📦 Installation ```bash npm install @appello/services ``` **Peer Dependencies:** ```bash npm install @appello/common ``` ## <a id="quick-start"></a>🚀 Quick Start ### Basic Setup ```typescript import { createApiService, createGqlClient } from '@appello/services'; // REST API Service const apiService = createApiService({ url: 'https://api.example.com', getAccessToken: () => localStorage.getItem('accessToken'), getRefreshToken: () => localStorage.getItem('refreshToken'), onTokenRefreshSuccess: tokens => { localStorage.setItem('accessToken', tokens.accessToken); localStorage.setItem('refreshToken', tokens.refreshToken); }, onTokenRefreshError: () => { // Handle logout }, refreshTokenUrl: '/auth/refresh', }); // GraphQL Client const gqlClient = createGqlClient({ url: 'https://api.example.com/graphql', wsUrl: 'wss://api.example.com/graphql', getAccessToken: () => localStorage.getItem('accessToken'), getRefreshToken: () => localStorage.getItem('refreshToken'), onTokenRefreshSuccess: tokens => { localStorage.setItem('accessToken', tokens.accessToken); localStorage.setItem('refreshToken', tokens.refreshToken); }, onTokenRefreshError: () => { // Handle logout }, refreshTokens: async (client, context) => { // Custom refresh logic }, }); ``` ### Error Handling ```typescript import { createProcessApiErrorResponse } from '@appello/services'; const processApiError = createProcessApiErrorResponse({ onGlobalError: message => toast.error(message), onUnknownErrors: message => console.error(message), }); ``` ## <a id="core-services"></a>🏗️ Core Services | Service | Purpose | Factory Function | | --------------- | ---------------------------------------- | --------------------------- | | --------------- | ---------------------------------------- | --------------------------- | | **REST API** | HTTP requests with auth & error handling | `createApiService(config)` | | **GraphQL** | Apollo Client with subscriptions & auth | `createGqlClient(config)` | | **React Query** | TanStack React Query client | `createQueryClient(config)` | ## <a id="integration-examples"></a>🔗 Integration Examples ### 🏪 RTK Query Integration ```ts import { createApi } from '@reduxjs/toolkit/query/react'; import { axiosBaseQuery, handleRtkQueryError } from '@appello/services'; export const api = createApi({ baseQuery: axiosBaseQuery({ api: apiService }), endpoints: builder => ({ login: builder.mutation({ query: credentials => ({ url: '/auth/login', method: 'POST', data: credentials, }), }), }), }); // Usage const [login] = api.useLoginMutation(); try { await login(credentials).unwrap(); } catch (error) { processError({ errors: handleRtkQueryError(error) }); } ``` ### 🔄 React Query Integration ```typescript import { useMutation } from '@tanstack/react-query'; import { handleApiRequestError } from '@appello/services'; const mutation = useMutation({ mutationFn: credentials => apiService.post('/auth/login', credentials), onError: error => { processError({ errors: handleApiRequestError({ error }) }); }, }); ``` ### 🎯 Form Integration ```typescript import { useForm } from 'react-hook-form'; const { handleSubmit, setError } = useForm(); const onSubmit = async data => { try { await apiService.post('/auth/login', data); } catch (error) { processError({ errors: handleApiRequestError({ error }), fields: ['username', 'password'], setFormError: setError, }); } }; ``` ### 🌐 GraphQL Setup ```typescript import { createGqlClient } from '@appello/services'; const gqlClient = createGqlClient({ url: 'https://api.example.com/graphql', wsUrl: 'wss://api.example.com/graphql', getAccessToken: () => localStorage.getItem('accessToken'), getRefreshToken: () => localStorage.getItem('refreshToken'), onTokenRefreshSuccess: tokens => { localStorage.setItem('accessToken', tokens.accessToken); localStorage.setItem('refreshToken', tokens.refreshToken); }, onTokenRefreshError: () => { window.location.href = '/login'; }, refreshTokens: async (client, context) => { const { data } = await client.mutate({ mutation: REFRESH_TOKEN_MUTATION, context, }); return data.refreshToken; }, }); ``` ## <a id="utilities"></a>🛠️ Utilities ### Error Handling Functions | Function | Purpose | Use Case | | --------------------------------------- | ------------------------ | -------------------------------------------- | | --------------------------------------- | ------------------------ | -------------------------------------------- | | `handleApiRequestError({ error })` | Process REST API errors | Convert Axios errors to user-friendly format | | `handleRtkQueryError(error)` | Process RTK Query errors | Extract error data from RTK Query responses | | `createProcessApiErrorResponse(config)` | Form error processor | Integrate API errors with React Hook Form | | `createProcessGqlErrorResponse(config)` | GraphQL error processor | Handle GraphQL errors in forms | ### Authentication Utilities #### 🔐 setAuthorizationHeader Adds Bearer token to request headers without mutating the original headers object. ```typescript import { setAuthorizationHeader } from '@appello/services'; const headers = { 'Content-Type': 'application/json' }; const token = 'my-jwt-token'; const newHeaders = setAuthorizationHeader(token, headers); // Result: { 'Content-Type': 'application/json', Authorization: 'Bearer my-jwt-token' } ``` **Parameters:** - `token: string` - The access token - `headers: AxiosRequestHeaders` - Existing headers object **Returns:** `AxiosRequestHeaders` - New headers object with Authorization header #### 📊 getGqlAuthorizationHeader Creates an authorization header object for GraphQL requests. ```typescript import { getGqlAuthorizationHeader } from '@appello/services'; const authHeader = getGqlAuthorizationHeader('my-jwt-token'); // Result: { Authorization: 'Bearer my-jwt-token' } ``` **Parameters:** - `token: string` - The access token **Returns:** `{ Authorization: string }` - Authorization header object ### Token Management #### 🔄 refreshTokens Handles JWT token refresh for expired authentication tokens with sophisticated queuing and retry mechanisms. ```typescript // This function is typically used internally by the API service // but can be customized through ApiServiceConfig ``` **Purpose:** Implements a comprehensive token refresh mechanism that prevents race conditions by ensuring only one refresh operation runs at a time, while queuing additional requests until the refresh completes. **Features:** - ✅ **Race Condition Prevention**: Only one refresh operation runs at a time - ✅ **Request Queuing**: Queues subsequent requests during refresh - ✅ **Custom Refresh Logic**: Supports both endpoint-based and custom refresh functions - ✅ **Automatic Retry**: Retries failed requests after successful token refresh - ✅ **Error Handling**: Proper cleanup on refresh failures **Configuration:** Configured through `ApiServiceConfig` in the API service setup. #### 🏪 handleRtkQueryError Extracts error data from RTK Query responses and converts them to a standardized format. ```typescript import { handleRtkQueryError } from '@appello/services'; try { await login(credentials).unwrap(); } catch (error) { const errors = handleRtkQueryError(error); processError({ errors }); } ``` **Parameters:** - `error: unknown` - RTK Query error object **Returns:** `ResponseErrors` - Standardized error format **Features:** - ✅ **RTK Query Integration**: Specifically designed for RTK Query error handling - ✅ **Error Type Detection**: Handles different RTK Query error types - ✅ **Standardized Output**: Converts to consistent `ResponseErrors` format - ✅ **Type Safety**: Full TypeScript support ### Form Error Processing #### ⚠️ handleApiRequestError Processes Axios API errors into a standardized `ResponseErrors` format. ```typescript import { handleApiRequestError } from '@appello/services'; try { await apiService.post('/api/endpoint', data); } catch (error) { const errors = handleApiRequestError({ error }); // errors: { field1: 'Error message', field2: 'Another error' } } ``` **Parameters:** - `error: AxiosError` - The Axios error object - `handleDetailError?: (error: ResponseErrors) => ResponseErrors` - Custom field error handler - `onManualHandleError?: (error: AxiosError) => ResponseErrors` - Override error handling **Returns:** `ResponseErrors` - Map of field names to error messages **Features:** - Handles network, server, and validation errors - Flattens nested field errors to dot notation - Takes first error from arrays - Supports custom error processing #### 📝 createProcessApiErrorResponse Creates a processor for integrating API errors with React Hook Form. ```typescript import { createProcessApiErrorResponse } from '@appello/services'; const processError = createProcessApiErrorResponse({ onGlobalError: message => toast.error(message), onUnknownErrors: message => console.error(message), }); // Usage with form const { setError } = useForm(); try { await apiService.post('/api/login', credentials); } catch (error) { processError({ errors: handleApiRequestError({ error }), fields: ['username', 'password'], setFormError: setError, }); } ``` **Configuration Options:** - `onGlobalError?: (message: string) => void` - Handle non-field errors - `onUnknownErrors?: (message: string) => void` - Handle unknown errors - `onUnhandledFieldErrors?: (errors: UnhandledFieldError[]) => void` - Handle unhandled fields **Usage Parameters:** - `errors: ResponseErrors` - Error object from `handleApiRequestError` - `fields: string[] | Record<string, string>` - Form fields to handle - `setFormError?: (name: string, error: { message: string }) => void` - React Hook Form setError #### 🔧 createProcessGqlErrorResponse Creates a processor for handling GraphQL errors in forms. ```typescript import { createProcessGqlErrorResponse } from '@appello/services'; const processGqlError = createProcessGqlErrorResponse({ onNonFieldError: message => toast.error(message), onUnknownError: message => console.error(message), }); // Usage with GraphQL errors const { setError } = useForm(); try { await apolloClient.mutate({ mutation: LOGIN_MUTATION, variables: credentials }); } catch (gqlError) { processGqlError(gqlError, { fields: ['username', 'password'], setFormError: setError, }); } ``` **Configuration Options:** - `onNonFieldError?: (message: string) => void` - Handle general errors - `onUnknownError?: (message: string) => void` - Handle unknown errors - `onUnhandledFieldErrors?: (errors: UnhandledFieldError[]) => void` - Handle unhandled fields **Usage Parameters:** - `gqlError: unknown` - GraphQL error object - `fields: string[] | Record<string, string>` - Form fields to handle - `setFormError?: (name: string, error: { message: string }) => void` - React Hook Form setError ### GraphQL Error Helpers #### 🔍 GraphQL Error Detection ```typescript import { getGqlErrors, getGqlError, isGqlUnauthorizedError, isGqlBusinessError, isGqlUnknownError, } from '@appello/services'; // Extract errors from GraphQL response const errors = getGqlErrors(gqlError); // Get first error const firstError = getGqlError(errors); // Check error types if (isGqlUnauthorizedError(errors)) { // Handle 401 unauthorized } if (isGqlBusinessError(errors)) { // Handle business logic errors } ``` ## <a id="configuration"></a>⚙️ Configuration ### API Service Configuration ```typescript interface ApiServiceConfig<T = ApiBaseTokens> { url: string; // Base API URL getAccessToken?: () => Promise<string | null>; // Access token getter getRefreshToken?: () => Promise<string | null>; // Refresh token getter onTokenRefreshSuccess: (tokens: T) => void; // Success callback onTokenRefreshError: (error?: unknown) => void; // Error callback refreshTokenUrl?: string; // Refresh endpoint refreshTokens?: (instance, error) => Promise<T>; // Custom refresh logic getAccessTokenFromRefreshRequest?: (data) => string; // Extract token from response axiosConfig?: CreateAxiosDefaults; // Axios configuration } ``` ### GraphQL Client Configuration ```typescript interface GqlClientConfig<T = GqlBaseTokens> { url: string; // GraphQL endpoint wsUrl?: string; // WebSocket URL for subscriptions getAccessToken: () => Promise<string | null>; // Access token getter getRefreshToken: () => Promise<string | null>; // Refresh token getter onTokenRefreshSuccess: (tokens: T) => void; // Success callback onTokenRefreshError: (error?: unknown) => void; // Error callback refreshTokens: (client, context) => Promise<T>; // Custom refresh logic refreshTokenOperationName?: string; // Refresh mutation name refreshTokenErrorMessage?: string; // Error message onUnknownError?: (message: string) => void; // Unknown error handler cache?: InMemoryCacheConfig; // Apollo cache config additionalLinks?: ApolloLink | ApolloLink[]; // Extra Apollo links uploadLinkParams?: object; // File upload config } ``` ## <a id="complete-example"></a>💡 Complete Example ```typescript import { createApiService, createGqlClient, createProcessApiErrorResponse, handleApiRequestError, } from '@appello/services'; import { toast } from 'react-toastify'; // Tokens management const getAccessToken = () => localStorage.getItem('accessToken'); const getRefreshToken = () => localStorage.getItem('refreshToken'); const handleTokenRefreshSuccess = tokens => { localStorage.setItem('accessToken', tokens.accessToken); localStorage.setItem('refreshToken', tokens.refreshToken); }; const handleTokenRefreshError = () => { localStorage.clear(); window.location.href = '/login'; }; // API Service export const apiService = createApiService({ url: process.env.REACT_APP_API_URL, getAccessToken, getRefreshToken, onTokenRefreshSuccess: handleTokenRefreshSuccess, onTokenRefreshError: handleTokenRefreshError, refreshTokenUrl: '/auth/refresh', }); // GraphQL Client export const gqlClient = createGqlClient({ url: `${process.env.REACT_APP_API_URL}/graphql`, wsUrl: `${process.env.REACT_APP_WS_URL}/graphql`, getAccessToken, getRefreshToken, onTokenRefreshSuccess: handleTokenRefreshSuccess, onTokenRefreshError: handleTokenRefreshError, refreshTokens: async (client, context) => { const { data } = await client.mutate({ mutation: REFRESH_TOKEN_MUTATION, context, }); return data.refreshToken; }, }); // Error Processing export const processApiError = createProcessApiErrorResponse({ onGlobalError: message => toast.error(message), onUnknownErrors: message => { console.error('Unknown API error:', message); toast.error('An unexpected error occurred'); }, }); // Usage in components export const useLogin = () => { const { setError } = useForm(); return useMutation({ mutationFn: credentials => apiService.post('/auth/login', credentials), onError: error => { processApiError({ errors: handleApiRequestError({ error }), fields: ['email', 'password'], setFormError: setError, }); }, }); }; ``` ## <a id="troubleshooting"></a>❓ Troubleshooting ### Common Issues **Token Refresh Not Working** - Ensure `refreshTokenUrl` is correct - Check `getRefreshToken` returns valid token - Verify `onTokenRefreshSuccess` saves tokens properly **GraphQL Subscriptions Failing** - Check `wsUrl` is accessible - Verify WebSocket connection in network tab - Ensure proper authentication context **Form Errors Not Showing** - Check field names match between API and form - Verify `setFormError` function is passed correctly - Use `onUnhandledFieldErrors` to debug unmapped fields **RTK Query Integration Issues** - Ensure `axiosBaseQuery` receives initialized API service - Check error handling in RTK Query endpoints - Verify error transformation logic ### Debug Mode Enable detailed logging: ```typescript const apiService = createApiService({ // ... config axiosConfig: { // Enable request/response logging transformRequest: [ (data, headers) => { console.log('API Request:', { data, headers }); return data; }, ], }, }); ``` --- **Package Version:** 4.0.1 **License:** ISC **Author:** Appello Software For more examples and advanced usage, check the test files in the repository.