Skip to main content

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

  1. Enable stale-while-revalidate

    const { data } = useProjects(undefined, {
    staleTime: 5 * 60 * 1000, // 5 minutes
    });
  2. Prefetch data

    const queryClient = useQueryClient();
    await queryClient.prefetchQuery({
    queryKey: ["project", id],
    queryFn: () => projects.get(id),
    });
  3. Use suspense for loading states

    const { data } = useProjects(undefined, {
    suspense: true,
    });
    // Component suspends during loading
  4. 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