| name | handle-api-service |
| description | Guide for creating API service functions following project patterns. Use when creating new API endpoints, service functions, or integrating backend APIs with React Query (TanStack Query). Includes patterns for GET (useQuery), POST/PUT/PATCH/DELETE (useMutation), type definitions, error handling, notification integration, and i18n support. IMPORTANT - This project uses i18n, so all notification messages must use useTranslation hook and t() function (see i18n.mdc rule). |
API Service Handler
This skill provides the standardized pattern for creating API service functions and integrating them with TanStack Query in React components.
IMPORTANT: This project uses react-i18next for internationalization. All notification messages MUST use useTranslation hook and t() function. See i18n.mdc rule for detailed guidelines.
Quick Reference
Service Function Pattern:
static functionName = async (params): Promise<ReturnType> => {
const res = await ginApiService<ResponseType>(`${this.BASE_URL}/endpoint`, {
method: 'POST',
body: JSON.stringify(data)
})
return res.data
}
Component Integration:
- GET:
const queryName = useQuery({ queryKey, queryFn })
- POST/PUT/PATCH/DELETE:
const mutationName = useMutation({ mutationFn, onSuccess, onError })
Core Rules
- GET requests: Do NOT specify
method: 'GET' (it's the default)
- useQuery: Do NOT use
onError or onSuccess callbacks - handle errors in the service function if needed
- useMutation: DO use
onSuccess and onError with notify for user feedback
- Naming convention:
query[Feature] for GET, mutation[Feature] for mutations
- Types: Define in
types/common.ts or inline with service
- i18n: Check if project has
src/i18n/ folder - if yes, MUST use useTranslation hook and t() function for all notification messages (follow i18n.mdc rule)
Pattern Details
1. Define Types (if needed)
export interface IUser {
name: string
email: string
}
2. Create Service Function
import { ginApiService } from '.'
import type { IUser } from '../../types/common'
export default class AuthService {
private static readonly BASE_URL = '/auth'
static getProfile = async (): Promise<IUser> => {
const res = await ginApiService<{ data: IUser }>(`${this.BASE_URL}/profile`)
return res.data
}
static login = async (username: string, password: string) => {
const res = await ginApiService(`${this.BASE_URL}/login`, {
method: 'POST',
body: JSON.stringify({ username, password })
})
return res.data
}
}
3. Integrate with Component
For GET (useQuery):
import { useQuery } from '@tanstack/react-query'
import AuthService from '../../../services/gin/auth.service'
const Component: React.FC = () => {
const queryUserProfile = useQuery({
queryKey: ['userProfile'],
queryFn: () => AuthService.getProfile(),
retry: 1
})
if (queryUserProfile.isPending) return <CircularProgress />
if (queryUserProfile.isError) return <Error />
const user = queryUserProfile.data
return <div>{user?.name}</div>
}
Key points:
- Name:
query[Feature]
- No
onError or onSuccess
- Handle loading/error states in component
- Access data via
.data property
For POST/PUT/PATCH/DELETE (useMutation):
Without i18n:
import { useMutation } from '@tanstack/react-query'
import { useNotify } from '../../../stores/notification/notification.selector'
import AuthService from '../../../services/gin/auth.service'
const Component: React.FC = () => {
const notify = useNotify()
const mutationLogin = useMutation({
mutationFn: (data: LoginFormData) => AuthService.login(data.username, data.password),
onSuccess: (data) => {
notify('Login successful', 'success')
},
onError: () => {
notify('Login failed', 'error')
}
})
const handleSubmit = (data: LoginFormData) => {
mutationLogin.mutate(data)
}
}
With i18n (if project has src/i18n/ folder):
import { useMutation } from '@tanstack/react-query'
import { useTranslation } from 'react-i18next'
import { useNotify } from '../../../stores/notification/notification.selector'
import AuthService from '../../../services/gin/auth.service'
const Component: React.FC = () => {
const { t } = useTranslation('auth')
const notify = useNotify()
const mutationLogin = useMutation({
mutationFn: (data: LoginFormData) => AuthService.login(data.username, data.password),
onSuccess: (data) => {
notify(t('login.success.loginSuccess'), 'success')
},
onError: () => {
notify(t('login.error.invalidCredentials'), 'error')
}
})
const handleSubmit = (data: LoginFormData) => {
mutationLogin.mutate(data)
}
}
Key points:
- Name:
mutation[Feature]
- MUST have
onSuccess and onError
- MUST use
notify for user feedback
- Import
useNotify hook
- Trigger with
.mutate(data)
- If project has i18n: Import
useTranslation, use t() for all messages, follow i18n.mdc rule
Complete Example
Step 1: Define Type
export interface IDocument {
id: string
title: string
status: string
}
Step 2: Create Service
import { ginApiService } from '.'
import type { IDocument } from '../../types/common'
export default class DocumentService {
private static readonly BASE_URL = '/documents'
static getDocument = async (id: string): Promise<IDocument> => {
const res = await ginApiService<{ data: IDocument }>(`${this.BASE_URL}/${id}`)
return res.data
}
static createDocument = async (title: string) => {
const res = await ginApiService(`${this.BASE_URL}`, {
method: 'POST',
body: JSON.stringify({ title })
})
return res.data
}
static updateDocument = async (id: string, data: Partial<IDocument>) => {
const res = await ginApiService(`${this.BASE_URL}/${id}`, {
method: 'PATCH',
body: JSON.stringify(data)
})
return res.data
}
static deleteDocument = async (id: string) => {
const res = await ginApiService(`${this.BASE_URL}/${id}`, {
method: 'DELETE'
})
return res.data
}
}
Step 3: Use in Component
Without i18n:
import { useQuery, useMutation } from '@tanstack/react-query'
import { useNotify } from '../../../stores/notification/notification.selector'
import DocumentService from '../../../services/gin/document.service'
const DocumentPage: React.FC = () => {
const notify = useNotify()
const queryDocument = useQuery({
queryKey: ['document', documentId],
queryFn: () => DocumentService.getDocument(documentId),
enabled: !!documentId
})
const mutationCreate = useMutation({
mutationFn: (title: string) => DocumentService.createDocument(title),
onSuccess: () => {
notify('Document created successfully', 'success')
queryClient.invalidateQueries(['documents'])
},
onError: () => {
notify('Failed to create document', 'error')
}
})
const mutationUpdate = useMutation({
mutationFn: ({ id, data }: { id: string; data: Partial<IDocument> }) =>
DocumentService.updateDocument(id, data),
onSuccess: () => {
notify('Document updated successfully', 'success')
},
onError: () => {
notify('Failed to update document', 'error')
}
})
const mutationDelete = useMutation({
mutationFn: (id: string) => DocumentService.deleteDocument(id),
onSuccess: () => {
notify('Document deleted successfully', 'success')
},
onError: () => {
notify('Failed to delete document', 'error')
}
})
}
With i18n (if project has src/i18n/ folder):
import { useQuery, useMutation } from '@tanstack/react-query'
import { useTranslation } from 'react-i18next'
import { useNotify } from '../../../stores/notification/notification.selector'
import DocumentService from '../../../services/gin/document.service'
const DocumentPage: React.FC = () => {
const { t } = useTranslation('document')
const notify = useNotify()
const queryDocument = useQuery({
queryKey: ['document', documentId],
queryFn: () => DocumentService.getDocument(documentId),
enabled: !!documentId
})
const mutationCreate = useMutation({
mutationFn: (title: string) => DocumentService.createDocument(title),
onSuccess: () => {
notify(t('create.success'), 'success')
queryClient.invalidateQueries(['documents'])
},
onError: () => {
notify(t('create.error'), 'error')
}
})
const mutationUpdate = useMutation({
mutationFn: ({ id, data }: { id: string; data: Partial<IDocument> }) =>
DocumentService.updateDocument(id, data),
onSuccess: () => {
notify(t('update.success'), 'success')
},
onError: () => {
notify(t('update.error'), 'error')
}
})
const mutationDelete = useMutation({
mutationFn: (id: string) => DocumentService.deleteDocument(id),
onSuccess: () => {
notify(t('delete.success'), 'success')
},
onError: () => {
notify(t('delete.error'), 'error')
}
})
}
Common Patterns
Query with Parameters
const queryDocuments = useQuery({
queryKey: ['documents', filters],
queryFn: () => DocumentService.getDocuments(filters),
enabled: !!filters
})
Mutation with Form Data
Without i18n:
const mutationUpload = useMutation({
mutationFn: (formData: FormData) => DocumentService.uploadFile(formData),
onSuccess: () => {
notify('File uploaded successfully', 'success')
form.reset()
},
onError: () => {
notify('Upload failed', 'error')
}
})
With i18n:
const { t } = useTranslation('upload')
const mutationUpload = useMutation({
mutationFn: (formData: FormData) => DocumentService.uploadFile(formData),
onSuccess: () => {
notify(t('success.fileUploaded'), 'success')
form.reset()
},
onError: () => {
notify(t('error.uploadFailed'), 'error')
}
})
Cache Invalidation
import { useQueryClient } from '@tanstack/react-query'
const queryClient = useQueryClient()
const mutationUpdate = useMutation({
mutationFn: (data) => Service.update(data),
onSuccess: () => {
queryClient.invalidateQueries(['documents'])
}
})
Error Handling
For useQuery:
- Errors are handled in component via
isError state
- No
onError callback needed
- If custom error handling needed, implement in service function
For useMutation:
- Always use
onError with notify
- Provide clear error messages to users
- Consider specific error types if needed
Best Practices
- Consistent naming: Use descriptive names that reflect the action
- Type safety: Always define return types for service functions
- User feedback: Every mutation should notify success/error
- Cache management: Invalidate queries after mutations when data changes
- Loading states: Handle
isLoading for better UX
- Error states: Always handle
isError gracefully
Checklist
When creating a new API service function: