@appello/services
v5.0.0
Published
Services package with api / graphql
Downloads
1,086
Readme
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
- 📦 Installation
- 🚀 Quick Start
- 🏗️ Core Services
- 🔗 Integration Examples
- 🛠️ Utilities
- ⚙️ Configuration
- 💡 Complete Example
- ❓ Troubleshooting
✨ 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
📦 Installation
npm install @appello/servicesPeer Dependencies:
npm install @appello/common🚀 Quick Start
Basic Setup
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
import { createProcessApiErrorResponse } from '@appello/services';
const processApiError = createProcessApiErrorResponse({
onGlobalError: message => toast.error(message),
onUnknownErrors: message => console.error(message),
});🏗️ 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) |
🔗 Integration Examples
🏪 RTK Query Integration
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
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
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
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;
},
});🛠️ 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.
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.
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.
// This function is typically used internally by the API service
// but can be customized through ApiServiceConfigPurpose: 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.
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
ResponseErrorsformat - ✅ Type Safety: Full TypeScript support
Form Error Processing
⚠️ handleApiRequestError
Processes Axios API errors into a standardized ResponseErrors format.
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.
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 fromhandleApiRequestErrorfields: string[] | Record<string, string>- Form fields to handlesetFormError?: (name: string, error: { message: string }) => void- React Hook Form setError
🔧 createProcessGqlErrorResponse
Creates a processor for handling GraphQL errors in forms.
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<string, string>- Form fields to handlesetFormError?: (name: string, error: { message: string }) => void- React Hook Form setError
GraphQL Error Helpers
🔍 GraphQL Error Detection
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
}⚙️ Configuration
API Service Configuration
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
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
}💡 Complete Example
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,
});
},
});
};❓ Troubleshooting
Common Issues
Token Refresh Not Working
- Ensure
refreshTokenUrlis correct - Check
getRefreshTokenreturns valid token - Verify
onTokenRefreshSuccesssaves tokens properly
GraphQL Subscriptions Failing
- Check
wsUrlis accessible - Verify WebSocket connection in network tab
- Ensure proper authentication context
Form Errors Not Showing
- Check field names match between API and form
- Verify
setFormErrorfunction is passed correctly - Use
onUnhandledFieldErrorsto debug unmapped fields
RTK Query Integration Issues
- Ensure
axiosBaseQueryreceives initialized API service - Check error handling in RTK Query endpoints
- Verify error transformation logic
Debug Mode
Enable detailed logging:
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.
