API Integration
How to communicate with the Yew Search backend API from the frontend.
Backend URL Configuration
The backend URL is configured in src/lib/config.ts:
import { config } from '$lib/config';
// Use in API calls
const url = `${config.BACKEND_URL}/v1/search`;
Default: http://localhost:3000 (the local-dev fallback in config.ts; real deployments set BACKEND_URL explicitly - see Local Deployment Setup).
Authentication
Don't call fetch() directly for authenticated requests - use the authenticatedFetch() wrapper from $lib/api.ts. It always sends credentials: 'include', and on a 401/403 it re-validates the session against GET /v1/user-session/me: if the session is actually invalid it clears auth state and redirects to /login, otherwise it passes the response through (a 403 with a valid session is a permissions issue, not an auth one, and the caller handles it).
import { authenticatedFetch } from '$lib/api';
import { config } from '$lib/config';
const response = await authenticatedFetch(`${config.BACKEND_URL}/v1/user`, {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
});
This is what search.svelte, user-integration.svelte, user-integration-by-id.svelte, and every other authenticated route actually use today - it replaced a hand-rolled fetch() + manual credentials: 'include' + manual 403-redirect pattern everywhere.
Common Patterns
Simple GET Request
From search.svelte:
const response = await authenticatedFetch(
`${config.BACKEND_URL}/v1/search?q=${encodeURIComponent(searchQuery)}`,
{ method: 'GET', headers: { 'Content-Type': 'application/json' } },
);
if (!response.ok) {
throw new Error('Failed to fetch search results');
}
const data = await response.json();
Paginated GET Request
From user-integration.svelte:
const skip = (currentPage - 1) * itemsPerPage;
const response = await authenticatedFetch(
`${config.BACKEND_URL}/v1/user-integration?skip=${skip}&limit=${itemsPerPage}`,
{ method: 'GET', headers: { 'Content-Type': 'application/json' } },
);
const data = await response.json();
const items = data.results;
const totalPages = Math.ceil(data.pagination.total / itemsPerPage);
Multiple Parallel API Calls
From user-integration-by-id.svelte:
const [userIntegrationResponse, integrationResponse, contentResponse] = await Promise.all([
authenticatedFetch(`${config.BACKEND_URL}/v1/user-integration/${id}`, {
method: 'GET',
}),
authenticatedFetch(`${config.BACKEND_URL}/v1/integration/${domain}`, {
method: 'GET',
}),
authenticatedFetch(`${config.BACKEND_URL}/v1/user-integration-content?...`, {
method: 'GET',
}),
]);
DELETE Request with Confirmation
From user-integration.svelte:
const response = await authenticatedFetch(
`${config.BACKEND_URL}/v1/user-integration/${integrationId}`,
{ method: 'DELETE' },
);
if (response.ok) {
toast.success('Success', {
description: 'Integration removed successfully',
});
// Update UI
} else {
toast.error('Error', {
description: 'Failed to remove integration. Please try again.',
});
}
Error Handling
authenticatedFetch() already handles the 401/403-means-session-expired case (see Authentication above) - you don't need to redirect to login yourself. Handle everything else with a normal try-catch:
try {
const response = await authenticatedFetch(`${config.BACKEND_URL}/v1/...`);
if (!response.ok) {
throw new Error('API request failed');
}
const data = await response.json();
// Handle success
} catch (error) {
console.error('Failed to fetch data:', error);
toast.error('Error', {
description: 'Failed to load data. Please try again.',
});
}
TypeScript Interfaces
Define interfaces for API responses:
interface UserIntegration {
id: string;
userId: string;
integrationId: string;
integrationDomain: string;
config: Record<string, any>;
status: 'active' | 'inactive' | 'error';
createdAt: string;
updatedAt: string;
}
interface PaginatedResponse<T> {
results: T[];
pagination: {
skip: number;
limit: number;
total: number;
};
}
Query Parameter Construction
Simple
const query = encodeURIComponent(searchQuery);
const url = `${config.BACKEND_URL}/v1/search?q=${query}`;
Multiple Parameters
const apiUrl = new URL(`${config.BACKEND_URL}/v1/search`);
apiUrl.searchParams.set('q', searchQuery);
apiUrl.searchParams.set('skip', skip.toString());
apiUrl.searchParams.set('limit', limit.toString());
const response = await authenticatedFetch(apiUrl.toString(), {
method: 'GET',
});
Array Parameters
const domains = ['gmail.com', 'example.com'];
const domainsQuery = domains
.map((d) => `inDomains[]=${encodeURIComponent(d)}`)
.join('&');
const url = `${config.BACKEND_URL}/v1/integration?${domainsQuery}`;
Loading States
let isLoading = $state(false);
async function fetchData() {
isLoading = true;
try {
const response = await authenticatedFetch(`${config.BACKEND_URL}/v1/...`);
const data = await response.json();
// Handle data
} finally {
isLoading = false; // Always reset loading state
}
}
Best Practices
- Use
authenticatedFetch(), not rawfetch(), for authenticated requests - Use TypeScript interfaces for all API responses
- Handle errors gracefully with try-catch and user feedback
- Show loading states during API calls
- Encode query parameters with
encodeURIComponent() - Reset loading states in
finallyblocks - Use toast notifications for user feedback on success/error