読み込み中
豆知識
This guide explains how to integrate new API endpoints into the Solo Boss frontend application. The system follows a three-layer pattern: Types → APIs → Queries.
⚠️ IMPORTANT: Before implementing any API calls, read API Query Best Practices to avoid N+1 queries, excessive API calls, and performance issues.
Public API Reference: /docs/api-reference — Scalar-rendered OpenAPI 3.2 browser API 仕様: /docs/api-reference
The API integration follows a consistent pattern across the codebase:
┌─────────────────────────────────────────────────────────┐
│ UI Components │
│ (Use hooks from queries.ts) │
└────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Queries Layer │
│ (TanStack Query hooks in queries.ts) │
│ - useQuery, useMutation, useInfiniteQuery │
└────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ APIs Layer │
│ (API functions in apis.ts) │
│ - Uses apiClient or apiGetAll │
└────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Types Layer │
│ (TypeScript interfaces in types.ts) │
│ - Request params, Response models │
└────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ API Client Service │
│ (src/services/api-client-service.ts) │
│ - Axios instance with auth interceptors │
└─────────────────────────────────────────────────────────┘
Create TypeScript interfaces for all request parameters and response models in src/features/[feature-name]/types.ts.
For list endpoints with pagination, extend GetAllParams:
import { GetAllParams } from "@/types"
export interface MyFeatureQueryParams extends GetAllParams {
start_date?: number
end_date?: number
status?: "draft" | "posted" | "reversed"
// Add feature-specific params
}
For simple query params without pagination:
export interface MyFeatureQueryParams {
start_date: number
end_date: number
filter?: string
}
Define the data structure returned by the API:
export interface MyFeatureItem {
id: string
name: string
amount: number
status: "active" | "inactive"
createdAt: string
}
// For paginated responses, the API returns PaginationResponse<MyFeatureItem>
// No need to define wrapper - use PaginationResponse<T> from @/types
export interface CreateMyFeaturePayload {
name: string
amount: number
description?: string
}
export interface UpdateMyFeaturePayload {
name?: string
amount?: number
description?: string
}
// src/features/my-feature/types.ts
import { GetAllParams } from "@/types"
// Query Parameters
export interface MyFeatureQueryParams extends GetAllParams {
start_date: number
end_date: number
status?: "draft" | "posted"
}
// Response Models
export interface MyFeatureItem {
id: string
name: string
amount: number
status: string
createdAt: string
}
// Request Payloads
export interface CreateMyFeaturePayload {
name: string
amount: number
}
export interface UpdateMyFeaturePayload {
name?: string
amount?: number
}
Create API functions in src/features/[feature-name]/apis.ts that use the base apiClient or apiGetAll helper.
apiGetAll for Paginated List EndpointsFor endpoints that return paginated data:
import { apiGetAll } from "@/services/api-client-service"
import { PaginationResponse } from "@/types"
import { MyFeatureQueryParams, MyFeatureItem } from "./types"
import { ENDPOINTS } from "@/constants"
export const myFeatureApis = {
getList: async ({
params,
}: {
params: MyFeatureQueryParams
}): Promise<PaginationResponse<MyFeatureItem>> => {
return await apiGetAll(ENDPOINTS.MY_FEATURE.GET_LIST, params)
},
}
apiClient for Custom EndpointsFor endpoints that need custom handling (POST, PUT, DELETE, or non-paginated GET):
import apiClient from "@/services/api-client-service"
import { APIResponse, PaginationResponse } from "@/types"
import {
MyFeatureQueryParams,
MyFeatureItem,
CreateMyFeaturePayload,
} from "./types"
import { ENDPOINTS } from "@/constants"
export const myFeatureApis = {
// GET with custom params
getItem: async ({
id,
}: {
id: string
}): Promise<APIResponse<MyFeatureItem>> => {
try {
const { data } = await apiClient.get(`${ENDPOINTS.MY_FEATURE.GET}/${id}`)
return data
} catch (error) {
console.error("Error fetching item:", error)
throw error
}
},
// POST request
create: async ({
payload,
}: {
payload: CreateMyFeaturePayload
}): Promise<APIResponse<MyFeatureItem>> => {
try {
const { data } = await apiClient.post(
ENDPOINTS.MY_FEATURE.CREATE,
payload
)
return data
} catch (error) {
console.error("Error creating item:", error)
throw error
}
},
// PUT request
update: async ({
id,
payload,
}: {
id: string
payload: UpdateMyFeaturePayload
}): Promise<APIResponse<MyFeatureItem>> => {
try {
const { data } = await apiClient.put(
`${ENDPOINTS.MY_FEATURE.UPDATE}/${id}`,
payload
)
return data
} catch (error) {
console.error("Error updating item:", error)
throw error
}
},
// DELETE request
delete: async ({ id }: { id: string }): Promise<APIResponse<void>> => {
try {
const { data } = await apiClient.delete(
`${ENDPOINTS.MY_FEATURE.DELETE}/${id}`
)
return data
} catch (error) {
console.error("Error deleting item:", error)
throw error
}
},
}
Don't forget to add your endpoints to src/constants/endpoints.ts:
export const ENDPOINTS = {
// ... existing endpoints
MY_FEATURE: {
GET_LIST: "/my-feature",
GET: "/my-feature",
CREATE: "/my-feature",
UPDATE: (id: string) => `/my-feature/${id}`,
DELETE: (id: string) => `/my-feature/${id}`,
},
}
Create TanStack Query hooks in src/features/[feature-name]/queries.ts that wrap your API functions.
For fetching data:
import { useQuery } from "@tanstack/react-query"
import { myFeatureApis } from "./apis"
import { MyFeatureQueryParams } from "./types"
import { constants } from "@/constants"
export const useMyFeatureQuery = ({
params,
enabled = true,
}: {
params: MyFeatureQueryParams
enabled?: boolean
}) => {
return useQuery({
queryKey: [constants.QUERY_KEYS.MY_FEATURE.GET_LIST, params],
queryFn: () => myFeatureApis.getList({ params }),
enabled,
})
}
For paginated lists with infinite scroll:
import { useInfiniteQuery } from "@tanstack/react-query"
import { myFeatureApis } from "./apis"
import { MyFeatureQueryParams } from "./types"
import { constants } from "@/constants"
export const useInfiniteMyFeatureQuery = ({
params,
enabled = true,
}: {
params: MyFeatureQueryParams
enabled?: boolean
}) => {
return useInfiniteQuery({
queryKey: [constants.QUERY_KEYS.MY_FEATURE.GET_LIST, "infinite", params],
queryFn: ({ pageParam = 0 }) =>
myFeatureApis.getList({
params: {
...params,
page_number: pageParam + 1,
page_size: params.page_size ?? 20,
},
}),
getNextPageParam: (lastPage) => {
return lastPage.data.pagination.hasNext
? lastPage.data.pagination.currentPage
: undefined
},
initialPageParam: 0,
enabled,
})
}
For creating, updating, or deleting data:
import { useMutation, useQueryClient } from "@tanstack/react-query"
import { myFeatureApis } from "./apis"
import { CreateMyFeaturePayload } from "./types"
import { constants } from "@/constants"
export const useCreateMyFeatureMutation = () => {
const queryClient = useQueryClient()
return useMutation({
mutationKey: [constants.QUERY_KEYS.MY_FEATURE.CREATE],
mutationFn: myFeatureApis.create,
onSuccess: () => {
// Invalidate and refetch related queries
queryClient.invalidateQueries({
queryKey: [constants.QUERY_KEYS.MY_FEATURE.GET_LIST],
})
},
})
}
export const useUpdateMyFeatureMutation = () => {
const queryClient = useQueryClient()
return useMutation({
mutationKey: [constants.QUERY_KEYS.MY_FEATURE.UPDATE],
mutationFn: myFeatureApis.update,
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: [constants.QUERY_KEYS.MY_FEATURE.GET_LIST],
})
},
})
}
export const useDeleteMyFeatureMutation = () => {
const queryClient = useQueryClient()
return useMutation({
mutationKey: [constants.QUERY_KEYS.MY_FEATURE.DELETE],
mutationFn: myFeatureApis.delete,
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: [constants.QUERY_KEYS.MY_FEATURE.GET_LIST],
})
},
})
}
Add your query keys to src/constants/query-keys.ts:
export const QUERY_KEYS = {
// ... existing keys
MY_FEATURE: {
GET_LIST: "getMyFeatureList",
GET: "getMyFeature",
CREATE: "createMyFeature",
UPDATE: "updateMyFeature",
DELETE: "deleteMyFeature",
},
}
import { useMyFeatureQuery } from "@/features/my-feature/queries"
export function MyFeatureComponent() {
const { data, isLoading, error } = useMyFeatureQuery({
params: {
start_date: 20250101,
end_date: 20250131,
page_size: 20,
page_number: 1,
},
})
if (isLoading) return <div>Loading...</div>
if (error) return <div>Error: {error.message}</div>
return (
<div>
{data?.data.items.map((item) => (
<div key={item.id}>{item.name}</div>
))}
</div>
)
}
import { useInfiniteMyFeatureQuery } from "@/features/my-feature/queries"
export function MyFeatureInfiniteList() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfiniteMyFeatureQuery({
params: {
start_date: 20250101,
end_date: 20250131,
page_size: 20,
},
})
return (
<div>
{data?.pages.map((page, i) => (
<div key={i}>
{page.data.items.map((item) => (
<div key={item.id}>{item.name}</div>
))}
</div>
))}
{hasNextPage && (
<button onClick={() => fetchNextPage()}>
{isFetchingNextPage ? "Loading..." : "Load More"}
</button>
)}
</div>
)
}
import { useCreateMyFeatureMutation } from "@/features/my-feature/queries"
import { toast } from "sonner"
export function CreateMyFeatureForm() {
const createMutation = useCreateMyFeatureMutation()
const handleSubmit = async (formData: CreateMyFeaturePayload) => {
try {
await createMutation.mutateAsync({ payload: formData })
toast.success("Item created successfully")
} catch (error) {
toast.error("Failed to create item")
}
}
return (
<form onSubmit={(e) => {
e.preventDefault()
handleSubmit({ name: "Test", amount: 1000 })
}}>
<button type="submit" disabled={createMutation.isPending}>
{createMutation.isPending ? "Creating..." : "Create"}
</button>
</form>
)
}
Every useQuery hook MUST include proper caching configuration to prevent excessive API calls:
// ✅ REQUIRED configuration for ALL queries
return useQuery({
queryKey: [QUERY_KEYS.FEATURE.ACTION, params],
queryFn: () => api.fetchData(params),
enabled: !!requiredParam, // Prevent unnecessary calls
staleTime: 60_000, // At least 30 seconds (60s recommended)
gcTime: 5 * 60_000, // Keep cache longer (5 min recommended)
refetchOnWindowFocus: false, // Prevent tab-switch refetch
refetchOnMount: false, // Use cached data if available
})
Common anti-patterns to avoid:
enabled option for conditional fetchingSee API Query Best Practices for detailed patterns.
APIResponse<T>, PaginationResponse<T>) from @/typesany types - use proper interfaces@/constants/query-keys for consistency[feature, operation, params]onSuccess callbacks of mutationsqueryClient.setQueryData for optimistic updatesapiGetAll helper for paginated endpointsuseInfiniteQuery with proper getNextPageParamhasNext and hasPrev flags from pagination responseGetAllParams for list endpoints with paginationpage_size, start_date)PaginationResponse<T> for paginated endpointsAPIResponse<T> for single-item endpointssrc/features/[feature-name]/
├── types.ts # TypeScript interfaces
├── apis.ts # API functions
├── queries.ts # TanStack Query hooks
└── components/ # UI components using queries
apiClient from @/services/api-client-service (includes auth headers)apiGetAll helper for paginated GET requests// types.ts
export interface MyListParams extends GetAllParams {
status?: string
}
// apis.ts
export const myApis = {
getList: async ({ params }: { params: MyListParams }) => {
return await apiGetAll(ENDPOINTS.MY_FEATURE.LIST, params)
},
}
// queries.ts
export const useMyListQuery = ({ params }: { params: MyListParams }) => {
return useQuery({
queryKey: [QUERY_KEYS.MY_FEATURE.LIST, params],
queryFn: () => myApis.getList({ params }),
})
}
// apis.ts
export const myApis = {
create: async ({ payload }: { payload: CreatePayload }) => {
const { data } = await apiClient.post(ENDPOINTS.MY_FEATURE.CREATE, payload)
return data
},
}
// queries.ts
export const useCreateMutation = () => {
const queryClient = useQueryClient()
return useMutation({
mutationFn: myApis.create,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.MY_FEATURE.LIST] })
},
})
}
// queries.ts
export const useInfiniteListQuery = ({ params }: { params: ListParams }) => {
return useInfiniteQuery({
queryKey: [QUERY_KEYS.MY_FEATURE.LIST, "infinite", params],
queryFn: ({ pageParam = 0 }) =>
myApis.getList({
params: { ...params, page_number: pageParam + 1 },
}),
getNextPageParam: (lastPage) =>
lastPage.data.pagination.hasNext
? lastPage.data.pagination.currentPage
: undefined,
initialPageParam: 0,
})
}
interface APIResponse<T> {
data: T
message: string
success: boolean
}
interface PaginationResponse<T> {
data: {
items: T[]
pagination: {
currentPage: number
pageSize: number
totalItems: number
totalPages: number
hasNext: boolean
hasPrev: boolean
}
}
message: string
success: boolean
}
interface GetAllParams {
page_size?: number
page_number?: number
search?: string
[key: string]: any
}
Solution: Ensure you're invalidating the correct query key in the mutation's onSuccess callback.
Solution: Verify your response types match the actual API response structure. Check the network tab to see the exact response format.
Solution: Ensure you're using apiGetAll for paginated endpoints and passing params correctly. Check that page_size and page_number are included in params.
Solution: The apiClient automatically handles auth tokens. Ensure cookies are set correctly and the token hasn't expired.
types.ts for requests and responsesapis.ts using apiClient or apiGetAllqueries.ts using TanStack QueryFollow this pattern consistently across all features for maintainable, type-safe API integration.
// src/features/sales-invoice/queries.ts
export const useGetSalesInvoicesQuery = ({ params, enabled = true }) => {
return useQuery({
queryKey: [QUERY_KEYS.SALES_INVOICE.GET_ALL, params],
queryFn: () => salesInvoiceApis.getAll({ params }),
enabled,
staleTime: 2 * 60 * 1000,
})
}
// src/features/company/queries.ts
export const useGetMyCompanyQuery = ({ enabled = true } = {}) => {
return useQuery({
queryKey: [QUERY_KEYS.COMPANY.GET_ME],
queryFn: () => companyApis.getMyCompany(),
enabled,
staleTime: 10 * 60 * 1000,
})
}
読み込み中
豆知識