API Calls & Type Generation
Quick reference for making type-safe API calls and managing code generation in your frontend.
tip
For comprehensive documentation, see the OpenAPI Fetch Documentation
How do I generate API types from the backend?
Set up automatic type generation:
# 1. Make sure your backend is running
# 2. Generate types from OpenAPI schema
npm run codegen --prefix front
# 3. The generated types will be in src/types/openapi.ts
How do I make API calls?
Basic CRUD operations:
// hooks/useTaskAPI.ts
import { useState } from 'react'
import { apiClient } from '@/types/openapi'
import { type Api } from '@/types/openapi'
export function useTaskAPI() {
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const createTask = async (taskData: Api["CreateTaskDTO"]): Promise<TaskDTO | null> => {
try {
setLoading(true)
setError(null)
const { data, error } = await apiClient.POST('/tasks/my-tasks', {
body: taskData
})
if (error) {
setError('Failed to create task')
return null
}
return data!
} catch (err) {
setError('Network error')
return null
} finally {
setLoading(false)
}
}
const getTasks = async (): Promise<Api["TaskDTO"][]> => {
try {
setLoading(true)
setError(null)
const { data, error } = await apiClient.GET('/tasks/my-tasks')
if (error) {
setError('Failed to fetch tasks')
return []
}
return data || []
} catch (err) {
setError('Network error')
return []
} finally {
setLoading(false)
}
}
const updateTask = async (taskId: string, updates: Api["UpdateTaskDTO"]): Promise<TaskDTO | null> => {
try {
setLoading(true)
setError(null)
const { data, error } = await apiClient.PUT('/tasks/{task_id}', {
params: { path: { task_id: taskId } },
body: updates
})
if (error) {
setError('Failed to update task')
return null
}
return data!
} catch (err) {
setError('Network error')
return null
} finally {
setLoading(false)
}
}
const deleteTask = async (taskId: string): Promise<boolean> => {
try {
setLoading(true)
setError(null)
const { error } = await apiClient.DELETE('/tasks/{task_id}', {
params: { path: { task_id: taskId } }
})
if (error) {
setError('Failed to delete task')
return false
}
return true
} catch (err) {
setError('Network error')
return false
} finally {
setLoading(false)
}
}
const toggleTask = async (taskId: string): Promise<Api["TaskDTO"] | null> => {
try {
setLoading(true)
setError(null)
const { data, error } = await apiClient.PATCH('/tasks/{task_id}/toggle', {
params: { path: { task_id: taskId } }
})
if (error) {
setError('Failed to toggle task')
return null
}
return data!
} catch (err) {
setError('Network error')
return null
} finally {
setLoading(false)
}
}
return {
createTask,
getTasks,
updateTask,
deleteTask,
toggleTask,
loading,
error
}
}
How do I handle query parameters?
API calls with filters and pagination:
// hooks/useTaskFilters.ts
import { apiClient } from '@/types/openapi'
import type { TaskDTO } from '@/types/openapi'
interface TaskFilters {
completed?: boolean
priority?: 'low' | 'medium' | 'high'
search?: string
page?: number
size?: number
}
export function useTaskFilters() {
const [loading, setLoading] = useState(false)
const getFilteredTasks = async (filters: TaskFilters = {}): Promise<{
tasks: TaskDTO[]
total: number
page: number
pages: number
}> => {
try {
setLoading(true)
// Type-safe query parameters
const { data, error } = await apiClient.GET('/tasks/', {
params: {
query: {
completed: filters.completed,
priority: filters.priority,
search: filters.search,
page: filters.page || 1,
size: filters.size || 20
}
}
})
if (error) {
throw new Error('Failed to fetch tasks')
}
return {
tasks: data?.tasks || [],
total: data?.pagination?.total || 0,
page: data?.pagination?.page || 1,
pages: data?.pagination?.pages || 0
}
} finally {
setLoading(false)
}
}
return { getFilteredTasks, loading }
}
// Usage in component
export function TaskList() {
const [filters, setFilters] = useState<TaskFilters>({})
const [tasks, setTasks] = useState<TaskDTO[]>([])
const { getFilteredTasks, loading } = useTaskFilters()
const loadTasks = async () => {
const result = await getFilteredTasks(filters)
setTasks(result.tasks)
}
const handleFilterChange = (newFilters: Partial<TaskFilters>) => {
setFilters(prev => ({ ...prev, ...newFilters }))
}
useEffect(() => {
loadTasks()
}, [filters])
return (
<div>
{/* Filter controls */}
<div className="flex gap-2 mb-4">
<select
onChange={(e) => handleFilterChange({
completed: e.target.value === 'all' ? undefined : e.target.value === 'true'
})}
>
<option value="all">All Tasks</option>
<option value="false">Pending</option>
<option value="true">Completed</option>
</select>
<select
onChange={(e) => handleFilterChange({
priority: e.target.value as any || undefined
})}
>
<option value="">Any Priority</option>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
</select>
</div>
{/* Task list */}
{loading ? (
<div>Loading...</div>
) : (
tasks.map(task => <TaskItem key={task.id} task={task} />)
)}
</div>
)
}
How do I set up request/response interceptors?
Add authentication and logging:
// services/apiClient.ts
import createClient from 'openapi-fetch'
import type { paths } from '@/types/openapi'
// Create custom client with interceptors
function createAPIClient() {
const client = createClient<paths>({
baseUrl: '/api'
})
// Request interceptor - add auth headers
client.use({
onRequest({ request }) {
// Add auth token if available
const token = localStorage.getItem('auth_token')
if (token) {
request.headers.set('Authorization', `Bearer ${token}`)
}
// Log request in development
if (process.env.NODE_ENV === 'development') {
console.log(`${request.method} request on ${request.url}`)
}
return request
},
onResponse({ response, request }) {
// Log response in development
if (process.env.NODE_ENV === 'development') {
const duration = performance.now() - (request as any).startTime
console.log(`${request.method} ${request.url} - ${response.status} (${duration.toFixed(2)}ms)`)
}
// Handle global errors
if (response.status === 401) {
// Redirect to login
window.location.href = '/login'
return
}
if (response.status >= 500) {
// Log server errors to monitoring
console.error('Server error:', response.status, response.statusText)
}
return response
}
})
return client
}
export const apiClient = createAPIClient()
Troubleshooting
Types not updating after backend changes?
- Restart your backend server (should be already restarted)
- Run
npm run codegen --prefix front
again - Restart your frontend dev server (should be already restarted)
API calls failing with CORS errors?
CORS: a browser security that blocks requests from another domain that does not have the right headers.
If you have an issue locally, check your vite.config.ts
proxy settings:
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://localhost:8000',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
})
This redirect all call from http://localhost:3000/api/*
(Vite dev server) to http://localhost:8000/
(FastAPI backend)