Services package with api / graphql
npm install @appello/servicesA comprehensive TypeScript library providing reusable service utilities for handling API requests, GraphQL operations, and data fetching across web and mobile React applications.
- ✨ Features
- 📦 Installation
- 🚀 Quick Start
- 🏗️ Core Services
- 🔗 Integration Examples
- 🛠️ Utilities
- ⚙️ Configuration
- 💡 Complete Example
- ❓ Troubleshooting
- 🌐 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
``bash`
npm install @appello/services
Peer Dependencies:
`bash`
npm install @appello/common
`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
},
});
`
`typescript
import { createProcessApiErrorResponse } from '@appello/services';
const processApiError = createProcessApiErrorResponse({
onGlobalError: message => toast.error(message),
onUnknownErrors: message => console.error(message),
});
`
| Service | Purpose | Factory Function |
| --------------- | ---------------------------------------- | --------------------------- |
| --------------- | ---------------------------------------- | --------------------------- |
| REST API | HTTP requests with auth & error handling | createApiService(config) |createGqlClient(config)
| GraphQL | Apollo Client with subscriptions & auth | |createQueryClient(config)
| React Query | TanStack React Query client | |
`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) });
}
`
`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 }) });
},
});
`
`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,
});
}
};
`
`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;
},
});
`
| 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 |
#### 🔐 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 tokenheaders: 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
#### 🔄 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
#### ⚠️ 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 objecthandleDetailError?: (error: ResponseErrors) => ResponseErrors
- - Custom field error handleronManualHandleError?: (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 errorsonUnknownErrors?: (message: string) => void
- - Handle unknown errorsonUnhandledFieldErrors?: (errors: UnhandledFieldError[]) => void
- - Handle unhandled fields
Usage Parameters:
- errors: ResponseErrors - Error object from handleApiRequestErrorfields: string[] | Record
- - Form fields to handlesetFormError?: (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 errorsonUnknownError?: (message: string) => void
- - Handle unknown errorsonUnhandledFieldErrors?: (errors: UnhandledFieldError[]) => void
- - Handle unhandled fields
Usage Parameters:
- gqlError: unknown - GraphQL error objectfields: string[] | Record
- - Form fields to handlesetFormError?: (name: string, error: { message: string }) => void
- - React Hook Form setError
#### 🔍 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
}
`
`typescript`
interface ApiServiceConfig
url: string; // Base API URL
getAccessToken?: () => Promise
getRefreshToken?: () => Promise
onTokenRefreshSuccess: (tokens: T) => void; // Success callback
onTokenRefreshError: (error?: unknown) => void; // Error callback
refreshTokenUrl?: string; // Refresh endpoint
refreshTokens?: (instance, error) => Promise
getAccessTokenFromRefreshRequest?: (data) => string; // Extract token from response
axiosConfig?: CreateAxiosDefaults; // Axios configuration
}
`typescript`
interface GqlClientConfig
url: string; // GraphQL endpoint
wsUrl?: string; // WebSocket URL for subscriptions
getAccessToken: () => Promise
getRefreshToken: () => Promise
onTokenRefreshSuccess: (tokens: T) => void; // Success callback
onTokenRefreshError: (error?: unknown) => void; // Error callback
refreshTokens: (client, context) => Promise
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
}
`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,${process.env.REACT_APP_WS_URL}/graphql
wsUrl: ,
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,
});
},
});
};
`
Token Refresh Not Working
- Ensure refreshTokenUrl is correctgetRefreshToken
- Check returns valid tokenonTokenRefreshSuccess
- Verify 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 correctlyonUnhandledFieldErrors
- Use 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
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.