A powerful React hooks library for handling API calls with streaming support, built on top of React Query and Axios.
- 🚀 Built on React Query and Axios
- 📡 Streaming support for real-time data
- 🎯 Type-safe API calls
- 🔄 Automatic retries and caching
- 🎨 Clean and simple API
- 📦 Reusable API definitions
npm install @ngtrvu/use-api
# or
yarn add @ngtrvu/use-api
# or
pnpm add @ngtrvu/use-api
The recommended way to organize your API calls is to create separate modules for different features:
// api/endpoints.ts - Central place for all API endpoints
export const ENDPOINTS = {
POSTS: {
LIST: '/api/posts',
DETAIL: (id: string) => `/api/posts/${id}`,
CREATE: '/api/posts/create',
UPDATE: (id: string) => `/api/posts/${id}`,
DELETE: (id: string) => `/api/posts/${id}`,
},
COMMENTS: {
LIST: (postId: string) => `/api/posts/${postId}/comments`,
CREATE: (postId: string) => `/api/posts/${postId}/comments`,
}
} as const
// api/types.ts - Type definitions
export interface Post {
id: string
title: string
content: string
author: string
}
export interface CreatePostParams {
title: string
content: string
}
export interface UpdatePostParams {
id: string
title?: string
content?: string
}
// api/posts.ts - Post-related API calls
import { apiCall } from '@ngtrvu/use-api'
import { ENDPOINTS } from './endpoints'
import type { Post, CreatePostParams, UpdatePostParams } from './types'
export const postsApi = {
list: apiCall<void, Post[]>('posts.list', () => ({
endpoint: ENDPOINTS.POSTS.LIST,
method: 'GET'
})),
detail: apiCall<{ id: string }, Post>('posts.detail', (params) => ({
endpoint: ENDPOINTS.POSTS.DETAIL(params.id),
method: 'GET'
})),
create: apiCall<CreatePostParams, Post>('posts.create', (params) => ({
endpoint: ENDPOINTS.POSTS.CREATE,
method: 'POST',
body: params
})),
update: apiCall<UpdatePostParams, Post>('posts.update', (params) => ({
endpoint: ENDPOINTS.POSTS.UPDATE(params.id),
method: 'PATCH',
body: params
})),
delete: apiCall<{ id: string }, void>('posts.delete', (params) => ({
endpoint: ENDPOINTS.POSTS.DELETE(params.id),
method: 'DELETE'
}))
}
// Usage in components
import { postsApi } from '../api/posts'
const PostList = () => {
const listMutation = useMutation(postsApi.list)
const deleteMutation = useMutation(postsApi.delete)
return (
<div>
{/* Component implementation */}
</div>
)
}
// api/chat/endpoints.ts
export const CHAT_ENDPOINTS = {
SEND: '/api/chat/send',
STREAM: '/api/chat/stream',
HISTORY: (userId: string) => `/api/chat/${userId}/history`
} as const
// api/chat/types.ts
export interface Message {
id: string
role: 'user' | 'assistant'
content: string
timestamp: string
}
export interface ChatParams {
messages: Message[]
model?: string
temperature?: number
}
export interface ChatResponse {
id: string
message: Message
usage: {
promptTokens: number
completionTokens: number
}
}
// api/chat/index.ts
import { apiCall } from '@ngtrvu/use-api'
import { CHAT_ENDPOINTS } from './endpoints'
import type { ChatParams, ChatResponse, Message } from './types'
export const chatApi = {
stream: apiCall<ChatParams, ChatResponse>('chat.stream', (params) => ({
endpoint: CHAT_ENDPOINTS.STREAM,
method: 'POST',
body: params,
streaming: true
})),
send: apiCall<ChatParams, ChatResponse>('chat.send', (params) => ({
endpoint: CHAT_ENDPOINTS.SEND,
method: 'POST',
body: params
})),
history: apiCall<{ userId: string }, Message[]>('chat.history', (params) => ({
endpoint: CHAT_ENDPOINTS.HISTORY(params.userId),
method: 'GET'
}))
}
// components/Chat.tsx
import { chatApi } from '../api/chat'
import type { Message } from '../api/chat/types'
const ChatComponent = () => {
const [messages, setMessages] = useState<Message[]>([])
const [currentResponse, setCurrentResponse] = useState('')
const chatMutation = useMutation(chatApi.stream, {
onStreaming: (chunk) => {
setCurrentResponse(prev => prev + chunk)
},
onSuccess: (data) => {
setMessages(prev => [...prev, data.message])
setCurrentResponse('')
}
})
return (
<div>{/* Chat UI implementation */}</div>
)
}
// api/upload/endpoints.ts
export const UPLOAD_ENDPOINTS = {
FILE: '/api/upload/file',
IMAGE: '/api/upload/image',
BULK: '/api/upload/bulk'
} as const
// api/upload/types.ts
export interface UploadProgress {
loaded: number
total: number
progress: number
}
export interface UploadResponse {
url: string
filename: string
size: number
}
// api/upload/index.ts
import { apiCall } from '@ngtrvu/use-api'
import { UPLOAD_ENDPOINTS } from './endpoints'
import type { UploadResponse } from './types'
export const uploadApi = {
file: apiCall<{ file: File }, UploadResponse>('upload.file', (params) => {
const formData = new FormData()
formData.append('file', params.file)
return {
endpoint: UPLOAD_ENDPOINTS.FILE,
method: 'POST',
body: formData,
streaming: true,
headers: {
'Content-Type': 'multipart/form-data'
}
}
}),
image: apiCall<{ image: File }, UploadResponse>('upload.image', (params) => {
const formData = new FormData()
formData.append('image', params.image)
return {
endpoint: UPLOAD_ENDPOINTS.IMAGE,
method: 'POST',
body: formData,
streaming: true,
headers: {
'Content-Type': 'multipart/form-data'
}
}
})
}
// components/FileUpload.tsx
import { uploadApi } from '../api/upload'
import type { UploadProgress } from '../api/upload/types'
const FileUploadComponent = () => {
const [progress, setProgress] = useState<UploadProgress>()
const uploadMutation = useMutation(uploadApi.file, {
onStreaming: (chunk: UploadProgress) => {
setProgress(chunk)
},
onSuccess: (data) => {
console.log('Upload complete:', data.url)
}
})
return (
<div>
<input
type="file"
onChange={(e) => {
const file = e.target.files?.[0]
if (file) uploadMutation.mutate({ file })
}}
/>
{progress && (
<progress value={progress.progress} max="100" />
)}
</div>
)
}
// api/account/constants.ts
export const ACCOUNT_ACTIONS = {
FETCH_DETAIL: 'FETCH_ACCOUNT_DETAIL',
UPDATE: 'UPDATE_ACCOUNT',
DELETE: 'DELETE_ACCOUNT'
} as const
// api/account/types.ts
export interface Account {
id: string
email: string
name: string
role: string
status: 'active' | 'inactive'
}
export interface UpdateAccountPayload {
name?: string
email?: string
status?: 'active' | 'inactive'
}
// api/account/index.ts
import appApiAxios from '@/utils/app-api-axios'
import { apiCall } from '@ngtrvu/use-api'
import { ACCOUNT_ACTIONS } from './constants'
import type { Account, UpdateAccountPayload } from './types'
export const accountApi = {
getDetail: apiCall<void, Account>(
ACCOUNT_ACTIONS.FETCH_DETAIL,
() => ({
endpoint: `/api/account`,
method: 'GET',
}),
appApiAxios
),
update: apiCall<UpdateAccountPayload, Account>(
ACCOUNT_ACTIONS.UPDATE,
(payload) => ({
endpoint: `/api/account`,
method: 'PATCH',
body: payload,
}),
appApiAxios
)
}
// Usage in components
import { accountApi } from '@/api/account'
const AccountSettings = () => {
const detailMutation = useMutation(accountApi.getDetail)
const updateMutation = useMutation(accountApi.update)
const handleUpdate = (data: UpdateAccountPayload) => {
updateMutation.mutate(data)
}
}
// utils/app-api-axios.ts
import axios from 'axios'
const appApiAxios = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL,
timeout: 30000,
headers: {
'Content-Type': 'application/json'
}
})
// Add request interceptor
appApiAxios.interceptors.request.use(
(config) => {
// Add auth token
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(error) => Promise.reject(error)
)
// Add response interceptor
appApiAxios.interceptors.response.use(
(response) => response,
(error) => {
// Handle common errors
if (error.response?.status === 401) {
// Handle unauthorized
}
return Promise.reject(error)
}
)
export default appApiAxios
// api/base.ts
import appApiAxios from '@/utils/app-api-axios'
import { apiCall } from '@ngtrvu/use-api'
export const createApiCall = <P = void, R = unknown>(
action: string,
configFn: (params: P) => {
endpoint: string
method: string
body?: any
streaming?: boolean
}
) => {
return apiCall<P, R>(action, configFn, appApiAxios)
}
// api/account/index.ts - Using createApiCall
import { createApiCall } from '../base'
import { ACCOUNT_ACTIONS } from './constants'
import type { Account, UpdateAccountPayload } from './types'
export const accountApi = {
getDetail: createApiCall<void, Account>(
ACCOUNT_ACTIONS.FETCH_DETAIL,
() => ({
endpoint: `/api/account`,
method: 'GET',
})
),
update: createApiCall<UpdateAccountPayload, Account>(
ACCOUNT_ACTIONS.UPDATE,
(payload) => ({
endpoint: `/api/account`,
method: 'PATCH',
body: payload,
})
)
}
/api
/base.ts // Base API configuration and utilities
/constants.ts // Shared constants
/account
/constants.ts // Account-specific constants
/types.ts // Account-related types
/index.ts // Account API definitions
/auth
/constants.ts // Auth-specific constants
/types.ts // Auth-related types
/index.ts // Auth API definitions
/posts
/constants.ts // Post-specific constants
/types.ts // Post-related types
/index.ts // Post API definitions
-
Use Action Constants
// constants.ts export const ACTIONS = { ACCOUNT: { FETCH: 'FETCH_ACCOUNT', UPDATE: 'UPDATE_ACCOUNT', }, AUTH: { LOGIN: 'LOGIN', LOGOUT: 'LOGOUT', } } as const
-
Type Everything
// types.ts export interface ApiResponse<T> { data: T message?: string status: number } export interface ApiError { code: string message: string status: number } // Usage const api = createApiCall<UpdateAccountPayload, ApiResponse<Account>>( ACTIONS.ACCOUNT.UPDATE, (payload) => ({ endpoint: '/api/account', method: 'PATCH', body: payload }) )
-
Group Related APIs
// api/account/index.ts export const accountApi = { // Account-related APIs detail: createApiCall(...), update: createApiCall(...), delete: createApiCall(...), // Account settings settings: { get: createApiCall(...), update: createApiCall(...) }, // Account preferences preferences: { get: createApiCall(...), update: createApiCall(...) } }
-
Use Environment Configuration
// config.ts export const API_CONFIG = { BASE_URL: process.env.NEXT_PUBLIC_API_URL, TIMEOUT: 30000, VERSION: 'v1', getEndpoint: (path: string) => `${API_CONFIG.BASE_URL}/${API_CONFIG.VERSION}${path}` } // Usage const api = createApiCall( 'ACTION', () => ({ endpoint: API_CONFIG.getEndpoint('/account'), method: 'GET' }) )
interface Options {
resourceName?: string; // Path to extract from response
onStreaming?: (chunk: unknown) => void; // Stream handler
onSuccess?: (data: unknown) => void; // Success callback
onError?: (error: Error) => void; // Error callback
}
const mutation = useMutation(apiCall, options)
interface ApiOptions {
endpoint: string; // API endpoint
method: string; // HTTP method
body?: any; // Request body
streaming?: boolean; // Enable streaming
}
const myApi = apiCall('name', (params) => ({
endpoint: '/api/endpoint',
method: 'POST',
body: params,
streaming: true
}))
MIT