Skip to main content

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​

  1. Use authenticatedFetch(), not raw fetch(), for authenticated requests
  2. Use TypeScript interfaces for all API responses
  3. Handle errors gracefully with try-catch and user feedback
  4. Show loading states during API calls
  5. Encode query parameters with encodeURIComponent()
  6. Reset loading states in finally blocks
  7. Use toast notifications for user feedback on success/error