SDK Best Practices
Follow these guidelines to get the most out of the DgiDgi SDK.
Always Use SDK - Never Direct Fetch
Critical Rule
NEVER make direct API calls. Always use the SDK.
// BAD - Direct fetch
const response = await fetch("/api/v1/projects");
const data = await response.json();
// BAD - Direct axios
const { data } = await axios.get("/api/v1/projects");
// GOOD - Use SDK hooks
const { projects } = useProjects();
// GOOD - Use SDK resources
const data = await projects.list();
Why?
- SDK handles authentication automatically
- SDK manages token refresh
- SDK provides type safety
- SDK handles errors consistently
- SDK enables caching with React Query
Prefer Hooks Over Resources
// BEST - Hooks with automatic caching
function ProjectList() {
const { projects } = useProjects();
// Automatic caching, refetching, loading states
}
// OK - Resources for non-React contexts
async function script() {
const data = await projects.list();
// No caching, manual error handling
}
// AVOID - Resources in React components
function BadComponent() {
const [data, setData] = useState(null);
useEffect(() => {
projects.list().then(setData); // Missing caching, loading states
}, []);
}
Handle Loading and Error States
// GOOD - Complete state handling
function ProjectList() {
const { projects } = useProjects();
if (projects.isLoading) {
return <Skeleton />;
}
if (projects.error) {
return <ErrorBoundary error={projects.error} />;
}
if (!projects.data?.data.length) {
return <EmptyState />;
}
return projects.data.data.map(p => <ProjectCard key={p.id} project={p} />);
}
// BAD - No state handling
function BadProjectList() {
const { projects } = useProjects();
return projects.data.data.map(p => <ProjectCard key={p.id} project={p} />);
// Crashes on loading, error, or empty
}
Use Conditional Fetching
// GOOD - Only fetch when needed
const { projects } = useProjects(workspaceId, {
enabled: !!workspaceId && isAuthenticated,
});
// BAD - Always fetches, may fail
const { projects } = useProjects(workspaceId);
// Fails if workspaceId is undefined
Invalidate Cache After Mutations
// GOOD - SDK hooks handle this automatically
const { actions: { create } } = useProjects();
await create.mutateAsync({ name: "New Project" });
// projects query is automatically invalidated
// GOOD - Manual invalidation when needed
const queryClient = useQueryClient();
await someCustomOperation();
queryClient.invalidateQueries({ queryKey: ["projects"] });
// BAD - Forgetting to invalidate
await projects.create({ name: "New" });
// List is stale, user doesn't see new project
Use Optimistic Updates
// GOOD - Optimistic update for better UX
const { actions: { update } } = useProject(id);
await update.mutateAsync(newData, {
onMutate: async (newData) => {
// Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey: ["project", id] });
// Snapshot previous value
const previous = queryClient.getQueryData(["project", id]);
// Optimistically update
queryClient.setQueryData(["project", id], (old) => ({
...old,
...newData,
}));
return { previous };
},
onError: (err, newData, context) => {
// Rollback on error
queryClient.setQueryData(["project", id], context.previous);
},
});
Handle Streaming Properly
// GOOD - Proper streaming with cleanup
function ChatComponent() {
const { streamMessage, cancelStream } = useStreamingChat();
// Cleanup on unmount
useEffect(() => {
return () => cancelStream();
}, [cancelStream]);
return /* ... */;
}
// BAD - No cleanup
function BadChat() {
const { streamMessage } = useStreamingChat();
// Stream continues after unmount, wastes resources
}
Type Your API Responses
// GOOD - Explicit types
const project = await projects.get<Project>(id);
// GOOD - Type inference from SDK
const { data } = useProject(id); // TypeScript knows data is Project | undefined
// BAD - Any type
const data: any = await client.get("/projects");
Error Handling Patterns
// GOOD - Comprehensive error handling
async function createProject(data: CreateProjectInput) {
try {
const project = await projects.create(data);
toast.success("Project created!");
return project;
} catch (error) {
if (error instanceof SDKError) {
switch (error.code) {
case "VALIDATION_ERROR":
toast.error("Invalid input: " + error.message);
break;
case "DUPLICATE":
toast.error("Project already exists");
break;
case "QUOTA_EXCEEDED":
toast.error("Project limit reached. Upgrade your plan.");
break;
default:
toast.error("Failed to create project");
}
}
throw error; // Re-throw for caller to handle
}
}
Pagination Best Practices
// GOOD - Proper pagination
function ProjectList() {
const [page, setPage] = useState(1);
const { projects } = useProjects(undefined, {
queryKey: ["projects", { page }],
keepPreviousData: true, // Smooth pagination
});
return (
<>
{projects.data?.data.map(p => <ProjectCard key={p.id} project={p} />)}
<Pagination
current={page}
total={projects.data?.total || 0}
pageSize={20}
onChange={setPage}
/>
</>
);
}
Environment Configuration
// GOOD - Environment-based configuration
const client = createClient({
baseURL: import.meta.env.VITE_API_URL || "/api/v1",
timeout: import.meta.env.PROD ? 30000 : 120000,
});
// BAD - Hardcoded values
const client = createClient({
baseURL: "http://localhost:5000/api/v1", // Breaks in production
});
Security Checklist
- Never store tokens in localStorage (SDK handles this)
- Never log full error objects (may contain secrets)
- Never expose API keys in client code
- Always use HTTPS in production
- Handle 401 errors with proper redirects
- Validate user input before API calls
- Use Content Security Policy headers
Performance Tips
-
Enable stale-while-revalidate
const { data } = useProjects(undefined, {
staleTime: 5 * 60 * 1000, // 5 minutes
}); -
Prefetch data
const queryClient = useQueryClient();
await queryClient.prefetchQuery({
queryKey: ["project", id],
queryFn: () => projects.get(id),
}); -
Use suspense for loading states
const { data } = useProjects(undefined, {
suspense: true,
});
// Component suspends during loading -
Debounce search queries
const debouncedSearch = useDebouncedValue(searchTerm, 300);
const { data } = useProjects(undefined, {
queryKey: ["projects", { search: debouncedSearch }],
enabled: debouncedSearch.length > 2,
});
Migration Guide
From Direct Fetch to SDK
// Before
const response = await fetch("/api/v1/projects", {
headers: { Authorization: `Bearer ${token}` },
});
const data = await response.json();
// After
import { projects } from "@dgidgi/sdk";
const data = await projects.list();
From Axios to SDK
// Before
const { data } = await axios.post("/api/v1/projects", { name: "New" });
// After
import { projects } from "@dgidgi/sdk";
const data = await projects.create({ name: "New" });
From Custom Hooks to SDK Hooks
// Before
function useProjects() {
const [data, setData] = useState(null);
useEffect(() => {
fetch("/api/v1/projects").then(r => r.json()).then(setData);
}, []);
return data;
}
// After
import { useProjects } from "@dgidgi/sdk";
// Just use it directly - caching, loading, errors handled