# openapi-fetch
# Overview
Use openapi-fetch (opens new window) as the primary HTTP client instead of traditional libraries like Axios.
# Why Replace Axios?
While Axios is flexible and widely used, it has several limitations:
❌ Problems with Axios
- No built-in type inference from API schema
- Requires manual typing for:
- Request payloads
- Response data
- Easy to introduce mismatch between frontend and backend contracts
- Error handling is loosely typed
# Why use openapi-fetch?
- Achieve end-to-end type safety
- Eliminate manual API typing
- Reduce runtime errors
- Improve developer experience
# Setup
# package.json
Packages & scripts
{
"name": "myapp",
"version": "1.0.0",
"scripts": {
"gen:ts-openapi": "openapi-typescript https://api.dev.abc.com.vn/swagger-json -o ./src/types/schema.d.ts --empty-objects-unknown --root-types --root-types-no-schema-prefix --make-paths-enum"
},
"dependencies": {
"openapi-fetch": "^0.15.0",
},
"devDependencies": {
"openapi-typescript": "^7.10.1",
}
}
gen:ts-openapiwill be fetch swagger json from backend and generate types for APIopenapi-fetchwill be use to fetch API with type safety
# Create API client
src/api/client.ts
import createClient from 'openapi-fetch'
import type { paths } from '@/types/schema'
export const apiClient = createClient<paths>({
baseUrl: 'https://api.dev.abc.com.vn',
querySerializer
})
# querySerializer
const querySerializer = (params: Record<string, any>) => {
const search = new URLSearchParams()
Object.entries(params).forEach(([key, value]) => {
if (value === undefined) return
switch (true) {
case value === null:
search.append(key, '')
break
case Array.isArray(value):
value.forEach((item) => {
search.append(`${key}`, String(item))
})
break
case typeof value === 'object':
search.append(key, JSON.stringify(value))
break
default:
search.append(key, String(value))
break
}
})
return search.toString()
}
# Usage
# with react-query
import { useQuery } from '@tanstack/react-query'
import { apiClient } from '@/api/client'
export function useGetBrandStoriesQuery() {
return useQuery({
queryKey: ['brand-stories'],
staleTime: 1000 * 60 * 5,
queryFn: async () => {
const { data: stories } = await apiClient.GET('/brands/stories')
return stories
},
})
}
# Error handling
Error only present when 4xx or 5xx
const { data, error } = await apiClient.GET('/brands/stories')
if (error) {
// handle error
if (error.status === 404) {
// Not found (typed)
}
if (error.status === 400) {
// Validation error (typed schema)
console.log(error.data)
}
throw new Error('Failed to fetch social posts')
}
Centralize error handling
const makeApiError = (error: ApiError<any>) => {
switch (error.status) {
case 401:
return {
type: "UNAUTHORIZED",
message: "Your session has expired. Please log in again.",
}
case 403:
return {
type: "FORBIDDEN",
message: "You don't have permission to perform this action.",
}
case 404:
return {
type: "NOT_FOUND",
message: "The requested resource could not be found.",
}
case 422:
return {
type: "VALIDATION_ERROR",
message: "Invalid input data.",
details: error.data, // typed validation errors
}
case 500:
return {
type: "SERVER_ERROR",
message: "Something went wrong on our end. Please try again later.",
}
default:
return {
type: "UNKNOWN_ERROR",
message: "An unexpected error occurred.",
}
}
}
Handle other cases: no internet, timeout, DNS fail, ...
try {
const { data, error } = await client.GET("/users")
if (error) {
throw makeApiError(error)
}
return data
} catch (e) {
// network-level error
return {
type: "NETWORK_ERROR",
message: "Please check your connection",
}
}
# openapi-react-query
A combination of openapi-fetch and react-query. Click here (opens new window) for detail.
← Sharing I18N for RN →