This guide explains how to use the auto-generated TypeScript SDK with TanStack Query for type-safe API calls in React components.
Laravel API (Swagger Annotations)
↓
L5-Swagger → OpenAPI 3.0 Spec (JSON)
↓
openapi-typescript-codegen → TypeScript SDK
↓
React Hooks + TanStack Query + Axios → Type-safe API calls
The SDK auto-generates on every npm run dev and npm run build.
npm run generate-sdkThis command uses a smart wrapper script that:
- Checks if Laravel server is running at
http://localhost:8888 - If available: Fetches OpenAPI spec and generates TypeScript SDK
- If not available: Skips generation gracefully (won't break your build)
If you need to regenerate the SDK after the Laravel server is already running:
npm run generate-sdk:forceThis command:
- Fetches OpenAPI spec from
http://localhost:8888/api/documentation/json - Generates TypeScript SDK in
resources/js/sdk/ - Creates type-safe models and services
- Requires Laravel server to be running
resources/js/
├── sdk/ # Auto-generated (gitignored)
│ ├── core/ # HTTP client
│ ├── models/ # TypeScript interfaces
│ ├── services/ # API services
│ └── index.ts # Main exports
├── lib/
│ ├── queryClient.ts # TanStack Query client config
│ └── api.ts # Axios instance with interceptors
└── hooks/
└── useProducts.ts # TanStack Query hooks
Configures TanStack Query with:
- staleTime: 5 minutes
- gcTime: 10 minutes
- retry: 1 attempt
- refetchOnWindowFocus: disabled
Axios instance with:
- Base URL:
/api/v1 - Request interceptor: Adds bearer token from localStorage
- Response interceptor: Handles 401 (redirects to login)
- Credentials: Included for CSRF
import { useProducts } from '@/hooks/useProducts';
function ProductsList() {
const { data, isLoading, error } = useProducts({
page: 1,
per_page: 10,
status: 'published',
});
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
{data?.data.map((product) => (
<div key={product.id}>{product.name}</div>
))}
</div>
);
}import { useProduct } from '@/hooks/useProducts';
function ProductDetail({ productId }: { productId: number }) {
const { data: product, isLoading } = useProduct(productId);
if (isLoading) return <div>Loading...</div>;
return <div>{product?.name}</div>;
}import { useCreateProduct } from '@/hooks/useProducts';
function CreateProductForm() {
const createProduct = useCreateProduct();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
await createProduct.mutateAsync({
name: 'New Product',
type: 'single',
price: 99.99,
stock_quantity: 100,
status: 'draft',
});
} catch (error) {
console.error('Failed to create product:', error);
}
};
return (
<form onSubmit={handleSubmit}>
<button disabled={createProduct.isPending}>
{createProduct.isPending ? 'Creating...' : 'Create Product'}
</button>
</form>
);
}import { useUpdateProduct } from '@/hooks/useProducts';
function EditProductForm({ productId }: { productId: number }) {
const updateProduct = useUpdateProduct();
const handleUpdate = async () => {
await updateProduct.mutateAsync({
id: productId,
name: 'Updated Name',
price: 149.99,
});
};
return (
<button onClick={handleUpdate} disabled={updateProduct.isPending}>
{updateProduct.isPending ? 'Updating...' : 'Update Product'}
</button>
);
}import { useDeleteProduct } from '@/hooks/useProducts';
function DeleteProductButton({ productId }: { productId: number }) {
const deleteProduct = useDeleteProduct();
const handleDelete = async () => {
if (confirm('Are you sure?')) {
await deleteProduct.mutateAsync(productId);
}
};
return (
<button onClick={handleDelete} disabled={deleteProduct.isPending}>
{deleteProduct.isPending ? 'Deleting...' : 'Delete'}
</button>
);
}const { data, error, isError } = useProducts();
if (isError) {
return <div>Error: {error.message}</div>;
}const createProduct = useCreateProduct();
try {
await createProduct.mutateAsync(data);
} catch (error) {
if (axios.isAxiosError(error)) {
console.error('API Error:', error.response?.data);
}
}Mutations automatically invalidate related queries:
// After creating a product, useProducts queries are invalidated
const createProduct = useCreateProduct();
await createProduct.mutateAsync(data);
// All useProducts queries will refetchimport { useQueryClient } from '@tanstack/react-query';
function MyComponent() {
const queryClient = useQueryClient();
const refreshProducts = () => {
queryClient.invalidateQueries({ queryKey: ['products'] });
};
return <button onClick={refreshProducts}>Refresh</button>;
}All types are auto-generated from the API and available in hooks:
import type { Product, CreateProductInput } from '@/hooks/useProducts';
const product: Product = {
id: 1,
name: 'Example',
type: 'single',
// ... full type safety
};- Use the hooks instead of calling the API directly
- Let TanStack Query handle caching - don't store API data in component state
- Use optimistic updates for better UX:
const updateProduct = useUpdateProduct();
await updateProduct.mutateAsync(data, {
onMutate: async (newProduct) => {
// Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey: ['products'] });
// Snapshot previous value
const previousProducts = queryClient.getQueryData(['products']);
// Optimistically update
queryClient.setQueryData(['products'], (old) => {
// Update logic
});
return { previousProducts };
},
onError: (err, newProduct, context) => {
// Rollback on error
queryClient.setQueryData(['products'], context?.previousProducts);
},
});- Handle loading and error states consistently
- Use proper TypeScript types from the hooks
ls -la resources/js/sdk/npm run generate-sdkVisit: http://localhost:8888/api/documentation/json
Check if:
- Laravel server is running (
php artisan serve) - Swagger docs are generated (
php artisan l5-swagger:generate) - OpenAPI spec is accessible at
/api/documentation/json
Make sure:
- User is authenticated
- Token is stored in localStorage as
auth_token - Sanctum middleware is applied to routes
Run:
npm run typesTo check for type errors without compilation.