MuSProt / frontend /src /api /protein.ts
wenruifan's picture
Deploy MuSProt React and FastAPI application
3993320
Raw
History Blame Contribute Delete
3.9 kB
/**
* API client for protein visualization backend
* Connects to FastAPI backend (default: http://localhost:8000)
*/
import axios from 'axios';
import type { FilterParams, FilterOptions, DataResponse, SummaryStats, ChainMetadata } from '../types/protein';
// API base URL - uses Vite proxy in development
const API_BASE_URL = import.meta.env.VITE_PROTEIN_API_BASE || '/api';
const api = axios.create({
baseURL: API_BASE_URL,
headers: {
'Content-Type': 'application/json',
},
});
// Enhanced error handling with diagnostics
api.interceptors.response.use(
(response) => response,
async (error) => {
const url = error.config?.url || 'unknown';
const method = error.config?.method?.toUpperCase() || 'REQUEST';
const status = error.response?.status || 'NO_RESPONSE';
const statusText = error.response?.statusText || 'Connection failed';
// Try to get response body
let responseBody = '';
if (error.response?.data) {
if (typeof error.response.data === 'string') {
responseBody = error.response.data;
} else {
responseBody = JSON.stringify(error.response.data);
}
}
const diagnosticMessage = `API Error: ${method} ${url}${status} ${statusText}${responseBody ? '\nResponse: ' + responseBody : ''}`;
console.error(diagnosticMessage);
// Re-throw with enhanced message
error.message = diagnosticMessage;
return Promise.reject(error);
}
);
export const proteinApi = {
/**
* Get available filter options
*/
async getFilters(): Promise<FilterOptions> {
const response = await api.get<FilterOptions>('/protein/filters');
return response.data;
},
/**
* Get filtered protein data
*/
async getData(filters: FilterParams): Promise<DataResponse> {
const response = await api.post<DataResponse>('/protein/data', filters);
return response.data;
},
/**
* Get summary statistics
*/
async getSummary(params?: Partial<FilterParams>): Promise<SummaryStats> {
const response = await api.get<SummaryStats>('/protein/summary', { params });
return response.data;
},
/**
* Resolve chain metadata from RCSB web API
* @param signal - AbortSignal for request cancellation
*/
async resolveChain(
pdb_id: string,
auth_asym_id: string,
options?: { use_cache?: boolean; signal?: AbortSignal }
): Promise<ChainMetadata> {
const { use_cache = true, signal } = options || {};
try {
const response = await api.get<ChainMetadata>('/protein/chain/resolve', {
params: { pdb_id, auth_asym_id, use_cache },
signal
});
return response.data;
} catch (error: any) {
// Normalize 404 errors
if (error.response?.status === 404) {
const notFoundError: any = new Error('Chain not found');
notFoundError.code = 'NOT_FOUND';
notFoundError.status = 404;
notFoundError.detail = error.response?.data?.detail || 'Chain not found in PDB';
throw notFoundError;
}
throw error;
}
},
/**
* Batch resolve multiple chains
*/
async batchResolveChains(chains: Array<{ pdb_id: string; auth_asym_id: string }>, use_cache: boolean = true): Promise<Record<string, ChainMetadata>> {
const response = await api.post<Record<string, ChainMetadata>>('/protein/chain/batch-resolve', chains, {
params: { use_cache }
});
return response.data;
},
/**
* Get ranked functional annotations for a chain from local TSV
*/
async getChainFunctions(pdb_id: string, auth_asym_id: string): Promise<string[]> {
const response = await api.get<{ functions: string[] }>('/protein/chain/functions', {
params: { pdb_id, auth_asym_id },
});
return response.data.functions;
},
};