API Integration
Learn how to integrate your ABP React frontend with the ABP Framework backend API. This guide covers client generation, authentication, error handling, and best practices for API communication.
Overview
ABP React uses a type-safe, auto-generated API client that provides seamless integration with your ABP backend. The client is generated from your backend's OpenAPI specification, ensuring type safety and consistency.
API Client Generation
Automatic Generation
The project uses @hey-api/openapi-ts
to automatically generate TypeScript clients from your ABP backend's OpenAPI specification.
Configuration
The API client generation is configured in openapi-ts.config.ts
:
import { defineConfig } from '@hey-api/openapi-ts';
export default defineConfig({
client: '@hey-api/client-axios',
input: 'https://your-api-url.com/swagger/v1/swagger.json',
output: './src/client',
types: {
enums: 'javascript',
},
});
Generate Client
To generate or update the API client:
pnpm generate-client
This command:
- Fetches the OpenAPI specification from your backend
- Generates TypeScript interfaces and client methods
- Creates type-safe API service functions
- Updates the client in the
src/client
directory
Client Structure
The generated client includes:
src/client/
├── index.ts # Main client exports
├── types.gen.ts # Generated TypeScript types
├── services.gen.ts # Generated service methods
└── core/ # Core client functionality
├── ApiError.ts # Error handling
├── ApiResult.ts # Response types
├── BaseHttpRequest.ts # Base HTTP client
└── request.ts # Request utilities
Authentication Integration
JWT Token Management
ABP React automatically handles JWT token authentication:
// src/lib/api-client.ts
import { ApiClient } from '@/client';
import { getSession } from 'next-auth/react';
const apiClient = new ApiClient({
BASE: process.env.NEXT_PUBLIC_API_URL,
WITH_CREDENTIALS: true,
CREDENTIALS: 'include',
});
// Automatic token injection
apiClient.request.config.interceptors.request.use(async (config) => {
const session = await getSession();
if (session?.accessToken) {
config.headers.Authorization = `Bearer ${session.accessToken}`;
}
return config;
});
export { apiClient };
Session Management
Integration with NextAuth.js for session handling:
// src/sessionOptions.ts
import { NextAuthOptions } from 'next-auth';
import { JWT } from 'next-auth/jwt';
export const authOptions: NextAuthOptions = {
providers: [
CredentialsProvider({
name: 'credentials',
credentials: {
username: { label: 'Username', type: 'text' },
password: { label: 'Password', type: 'password' },
},
async authorize(credentials) {
// ABP authentication logic
const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/account/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentials),
});
if (response.ok) {
return await response.json();
}
return null;
},
}),
],
callbacks: {
async jwt({ token, user }) {
if (user) {
token.accessToken = user.accessToken;
token.refreshToken = user.refreshToken;
}
return token;
},
async session({ session, token }) {
session.accessToken = token.accessToken;
return session;
},
},
};
Making API Calls
Using Generated Services
The generated client provides type-safe service methods:
// Example: User management
import { UserService } from '@/client';
// Get users with pagination
const getUsersPage = async (page: number, pageSize: number) => {
const response = await UserService.getUserList({
skipCount: (page - 1) * pageSize,
maxResultCount: pageSize,
});
return response;
};
// Create a new user
const createUser = async (userData: CreateUserDto) => {
const response = await UserService.createUser({
requestBody: userData,
});
return response;
};
// Update user
const updateUser = async (id: string, userData: UpdateUserDto) => {
const response = await UserService.updateUser({
id,
requestBody: userData,
});
return response;
};
// Delete user
const deleteUser = async (id: string) => {
await UserService.deleteUser({ id });
};
Custom Hooks for API Operations
Create custom hooks for common API operations:
// src/hooks/useUsers.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { UserService } from '@/client';
import type { CreateUserDto, UpdateUserDto } from '@/client/types.gen';
export const useUsers = (page: number, pageSize: number) => {
return useQuery({
queryKey: ['users', page, pageSize],
queryFn: () => UserService.getUserList({
skipCount: (page - 1) * pageSize,
maxResultCount: pageSize,
}),
});
};
export const useUser = (id: string) => {
return useQuery({
queryKey: ['user', id],
queryFn: () => UserService.getUser({ id }),
enabled: !!id,
});
};
export const useCreateUser = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (userData: CreateUserDto) =>
UserService.createUser({ requestBody: userData }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] });
},
});
};
export const useUpdateUser = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, userData }: { id: string; userData: UpdateUserDto }) =>
UserService.updateUser({ id, requestBody: userData }),
onSuccess: (_, { id }) => {
queryClient.invalidateQueries({ queryKey: ['users'] });
queryClient.invalidateQueries({ queryKey: ['user', id] });
},
});
};
export const useDeleteUser = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: string) => UserService.deleteUser({ id }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] });
},
});
};
Error Handling
Global Error Handler
Implement global error handling for API calls:
// src/lib/error-handler.ts
import { ApiError } from '@/client';
import { toast } from 'sonner';
export const handleApiError = (error: unknown) => {
if (error instanceof ApiError) {
// ABP validation errors
if (error.status === 400 && error.body?.error?.validationErrors) {
const validationErrors = error.body.error.validationErrors;
Object.entries(validationErrors).forEach(([field, messages]) => {
toast.error(`${field}: ${messages.join(', ')}`);
});
return;
}
// ABP business logic errors
if (error.body?.error?.message) {
toast.error(error.body.error.message);
return;
}
// HTTP status errors
switch (error.status) {
case 401:
toast.error('Please log in to continue');
// Redirect to login
break;
case 403:
toast.error('You do not have permission to perform this action');
break;
case 404:
toast.error('Resource not found');
break;
case 500:
toast.error('An internal server error occurred');
break;
default:
toast.error('An unexpected error occurred');
}
} else {
// Network or other errors
toast.error('Unable to connect to the server');
}
};
React Query Error Handling
Integrate error handling with React Query:
// src/lib/query-client.ts
import { QueryClient } from '@tanstack/react-query';
import { handleApiError } from './error-handler';
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: (failureCount, error) => {
// Don't retry on authentication errors
if (error instanceof ApiError && error.status === 401) {
return false;
}
return failureCount < 3;
},
onError: handleApiError,
},
mutations: {
onError: handleApiError,
},
},
});
Multi-tenancy Support
Tenant Header Management
Handle multi-tenant scenarios:
// src/lib/tenant-client.ts
import { apiClient } from './api-client';
import { useTenant } from '@/hooks/useTenant';
// Tenant header interceptor
apiClient.request.config.interceptors.request.use((config) => {
const { currentTenant } = useTenant();
if (currentTenant) {
config.headers['__tenant'] = currentTenant.id;
}
return config;
});
Tenant-specific API Calls
// src/hooks/useTenantUsers.ts
import { useQuery } from '@tanstack/react-query';
import { UserService } from '@/client';
import { useTenant } from './useTenant';
export const useTenantUsers = (page: number, pageSize: number) => {
const { currentTenant } = useTenant();
return useQuery({
queryKey: ['users', currentTenant?.id, page, pageSize],
queryFn: () => UserService.getUserList({
skipCount: (page - 1) * pageSize,
maxResultCount: pageSize,
}),
enabled: !!currentTenant,
});
};
Performance Optimization
Request Deduplication
React Query automatically deduplicates requests:
// Multiple components calling the same API will share the result
const UserProfile = ({ userId }: { userId: string }) => {
const { data: user } = useUser(userId); // Shared across components
return <div>{user?.name}</div>;
};
Caching Strategy
Configure caching for different data types:
// src/hooks/useUsers.ts
export const useUsers = (page: number, pageSize: number) => {
return useQuery({
queryKey: ['users', page, pageSize],
queryFn: () => UserService.getUserList({
skipCount: (page - 1) * pageSize,
maxResultCount: pageSize,
}),
staleTime: 5 * 60 * 1000, // 5 minutes
cacheTime: 10 * 60 * 1000, // 10 minutes
});
};
Background Updates
Keep data fresh with background updates:
export const useUserProfile = (userId: string) => {
return useQuery({
queryKey: ['user', userId],
queryFn: () => UserService.getUser({ id: userId }),
refetchOnWindowFocus: true,
refetchInterval: 5 * 60 * 1000, // Refresh every 5 minutes
});
};
Testing API Integration
Mocking API Calls
Use Mock Service Worker (MSW) for testing:
// src/mocks/handlers.ts
import { rest } from 'msw';
export const handlers = [
rest.get('/api/users', (req, res, ctx) => {
return res(
ctx.json({
items: [
{ id: '1', name: 'John Doe', email: 'john@example.com' },
{ id: '2', name: 'Jane Smith', email: 'jane@example.com' },
],
totalCount: 2,
})
);
}),
rest.post('/api/users', (req, res, ctx) => {
return res(
ctx.json({
id: '3',
name: 'New User',
email: 'new@example.com',
})
);
}),
];
Testing Custom Hooks
// src/hooks/__tests__/useUsers.test.ts
import { renderHook, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useUsers } from '../useUsers';
const createWrapper = () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
},
});
return ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
);
};
describe('useUsers', () => {
it('should fetch users successfully', async () => {
const { result } = renderHook(() => useUsers(1, 10), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(result.current.data?.items).toHaveLength(2);
});
});
Best Practices
1. Type Safety
Always use the generated types for API calls:
// Good: Using generated types
const createUser = async (userData: CreateUserDto) => {
return UserService.createUser({ requestBody: userData });
};
// Bad: Using any type
const createUser = async (userData: any) => {
return UserService.createUser({ requestBody: userData });
};
2. Error Boundaries
Implement error boundaries for API error handling:
// src/components/ErrorBoundary.tsx
import { Component, ErrorInfo, ReactNode } from 'react';
interface Props {
children: ReactNode;
}
interface State {
hasError: boolean;
}
export class ApiErrorBoundary extends Component<Props, State> {
public state: State = {
hasError: false,
};
public static getDerivedStateFromError(_: Error): State {
return { hasError: true };
}
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('API Error:', error, errorInfo);
}
public render() {
if (this.state.hasError) {
return <div>Something went wrong with the API call.</div>;
}
return this.props.children;
}
}
3. Loading States
Always handle loading states in your UI:
const UserList = () => {
const { data, isLoading, error } = useUsers(1, 10);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error loading users</div>;
return (
<div>
{data?.items.map(user => (
<div key={user.id}>{user.name}</div>
))}
</div>
);
};
4. Optimistic Updates
Implement optimistic updates for better UX:
export const useUpdateUser = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, userData }: { id: string; userData: UpdateUserDto }) =>
UserService.updateUser({ id, requestBody: userData }),
onMutate: async ({ id, userData }) => {
// Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey: ['user', id] });
// Snapshot previous value
const previousUser = queryClient.getQueryData(['user', id]);
// Optimistically update
queryClient.setQueryData(['user', id], (old: any) => ({
...old,
...userData,
}));
return { previousUser };
},
onError: (err, { id }, context) => {
// Rollback on error
queryClient.setQueryData(['user', id], context?.previousUser);
},
onSettled: (_, __, { id }) => {
// Always refetch after error or success
queryClient.invalidateQueries({ queryKey: ['user', id] });
},
});
};
Troubleshooting
Common Issues
- CORS Errors: Ensure your ABP backend is configured to allow requests from your frontend domain
- Authentication Failures: Check that JWT tokens are being properly included in requests
- Type Mismatches: Regenerate the client after backend changes
- Network Timeouts: Configure appropriate timeout values for your API calls
Debug Mode
Enable debug mode for detailed API logging:
// src/lib/api-client.ts
if (process.env.NODE_ENV === 'development') {
apiClient.request.config.interceptors.request.use((config) => {
console.log('API Request:', config);
return config;
});
apiClient.request.config.interceptors.response.use(
(response) => {
console.log('API Response:', response);
return response;
},
(error) => {
console.error('API Error:', error);
return Promise.reject(error);
}
);
}
Next Steps
- Authentication - Set up authentication with ABP
- Multi-tenancy - Configure multi-tenant support
- Custom Hooks - Create reusable API hooks
- Testing - Test your API integration
Proper API integration is crucial for a successful ABP React application. Follow these patterns and best practices to ensure reliable, performant, and maintainable API communication.