@appello/services
Version:
Services package with api / graphql
602 lines (462 loc) • 18.1 kB
Markdown
# 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.