Skip to Content
ContributeFrontendAPI Integration

API Integration

This document explains how the Rhesis frontend integrates with the Rhesis backend API.

Architecture: BFF Proxy

The browser never holds a backend access token. Client components call a same-origin /api/backend/* proxy, which injects Authorization server-side from the httpOnly session cookie. Server-side code (Server Components, Route Handlers) calls the backend directly with BACKEND_URL. See apps/frontend/AGENTS.md’s “BFF Auth Pattern” section for the full contract.

src/utils/url-resolver.ts
export function getBaseUrl(): string {
if (typeof window === 'undefined') {
    return getServerBackendUrl(); // BACKEND_URL, container-to-container
} else {
    return `${window.location.origin}/api/backend`; // same-origin BFF proxy
}
}

API Client Implementation

The client is implemented in src/utils/api-client/ as flat per-resource files (projects-client.ts, tests-client.ts, tasks-client.ts, …), not an endpoints/ subfolder:

API Client Structure
└── 
src/utils/api-client
    ├── 
base-client.ts# BaseApiClient — headers, fetch, retry, error handling
    ├── 
client-factory.ts# ApiClientFactory — client-side, no token
    ├── 
server-factory.ts# createServerApiFactory() — server-side, with token
    ├── 
projects-client.ts# ProjectsClient extends BaseApiClient
    ├── 
...# ~40 other *-client.ts files, one per resource
    └── 
interfaces# TypeScript types per resource (not "types/")

Base Client

src/utils/api-client/base-client.ts
export class BaseApiClient {
constructor(
    protected sessionToken?: string,
    protected retryConfig: Partial<RetryConfig> = {},
    protected projectId?: string,
) {
    this.baseUrl = getBaseUrl();
}

private buildAuthHeaders(): HeadersInit {
    // Only attaches Authorization server-side. On the client, auth is
    // injected by the /api/backend proxy — a client-passed token is dropped.
    if (typeof window === 'undefined' && this.sessionToken) {
      return { Authorization: `Bearer ${this.sessionToken}` };
    }
    return {};
}

protected async fetch<T>(path: string, options: RequestInit = {}): Promise<T> {
    // Retries with backoff, parses structured errors, and signs out the
    // user on a 401 from the backend.
    ...
}

protected async fetchPaginated<T>(path: string, options: RequestInit = {}): Promise<Paginated<T>> {
    ...
}
}

Always construct new ApiClientFactory() with no arguments in client components/hooks/utils. Only server-side code (via createServerApiFactory()) passes an explicit token/projectId.

Resource Clients

src/utils/api-client/projects-client.ts
export class ProjectsClient extends BaseApiClient {
async getProject(identifier: string) {
    return this.fetch<Project>(`/projects/${identifier}`);
}

async createProject(data: ProjectCreate) {
    return this.fetch<Project>('/projects', { method: 'POST', body: JSON.stringify(data) });
}
}

Consumed through a factory rather than imported directly:

Usage
// Client component/hook — no token
const project = await new ApiClientFactory().getProjectsClient().getProject(identifier);

// Server Component / Route Handler — attaches the session token
const factory = await createServerApiFactory();
const project = await factory.getProjectsClient().getProject(identifier);

Proxy Routes

Two Next.js Route Handlers proxy to the backend:

  • src/app/api/backend/[...path]/route.ts — the BFF route hit by BaseApiClient on the client. Reads the access token via getFreshAccessToken() from the session cookie and injects Authorization; returns 401 if there’s no valid session.
  • src/app/api/[...path]/route.ts — a catch-all proxy (proxyToBackend()) for routes without a dedicated handler. Forwards an allow-list of headers (authorization, content-type, accept, x-project-id, …), applies per-path timeout budgets (longer for LLM/import/Garak calls), and follows same-origin backend redirects.

Avoid next.config.mjs rewrites for backend proxying — rewrite destinations are baked into the build at compile time, which doesn’t work for a runtime-configurable BACKEND_URL.

Type Definitions

API types live in src/utils/api-client/interfaces/ (singular resource files, e.g. project.ts) and mirror the backend’s snake_case fields rather than being camelCased:

src/utils/api-client/interfaces/project.ts
export interface Project extends ProjectBase {
id: string;
is_active: boolean;
user_id: string;
owner_id: string;
organization_id: string;
created_at?: string;
updated_at?: string;
// plus nested user/owner/organization objects and frontend-only fields
}

WebSocket API for Playground Chat

In addition to REST clients, the frontend Playground uses WebSocket events for interactive endpoint chat. The shared message types live in:

  • apps/frontend/src/utils/websocket/types.ts
  • apps/backend/src/rhesis/backend/app/schemas/websocket.py

Core chat event types:

  • chat.message
  • chat.response
  • chat.error

chat.message payload (frontend → backend)

FieldRequiredDescription
endpoint_idYesUUID of the endpoint to invoke
messageYesUser message text
conversation_idNoConversation continuity identifier
filesNoAttachments with filename, content_type, data
chat-message.json
{
  "type": "chat.message",
  "correlation_id": "corr_123",
  "payload": {
    "endpoint_id": "6f3b...",
    "message": "Analyze the attached file",
    "conversation_id": "session-abc",
    "files": [
      {
        "filename": "input.json",
        "content_type": "application/json",
        "data": "<base64>"
      }
    ]
  }
}

chat.response payload (backend → frontend)

FieldRequiredDescription
outputYesEndpoint response text
endpoint_idYesInvoked endpoint ID
trace_idNoTrace identifier for opening the trace drawer
conversation_idNoCanonical conversation ID returned by backend
output_filesNoFiles returned by the endpoint

All WebSocket traffic on the /ws endpoint (not just Playground chat) is capped at 10 MB per message.

Runtime Configuration

The backend URL is not baked into the client bundle at build time. app/layout.tsx injects it into every server-rendered page as window.__ENV__:

src/app/layout.tsx
const runtimeEnvScript = `window.__ENV__=${JSON.stringify({
apiBaseUrl: process.env.API_BASE_URL ?? 'http://localhost:8080',
})};`;

getClientApiBaseUrl() (utils/url-resolver.ts) reads window.__ENV__.apiBaseUrl — this is used by pages that call the backend directly and unauthenticated from the browser (login, magic-link, forgot-password, provider discovery), not by BaseApiClient, which always uses the same-origin /api/backend proxy regardless of environment.

.env.local
# Server-side: used directly by server code and injected into window.__ENV__
API_BASE_URL=http://localhost:8080

# Server-side: target for the /api/backend and /api/[...path] proxies
BACKEND_URL=http://localhost:8080

There is no NEXT_PUBLIC_API_BASE_URL env var and no build-time placeholder-replacement script — the same built image works across environments because the backend URL is read from process.env at request time, not baked in.

Error Handling

BaseApiClient.fetch() centralizes error handling: structured error parsing, retry with backoff on transient failures, and automatic session clearing on a 401 from the backend. Components can still add their own error.tsx boundary for UI-level fallbacks (see Routing).