Skip to main content

OpraHttpClient

OpraHttpClient is the ready-to-use HTTP client for consuming OPRA services. It wires FetchBackend automatically — no extra configuration is required beyond a service URL.

Package: @opra/client
Extends: HttpClientBase

import { OpraHttpClient } from '@opra/client';

const client = new OpraHttpClient('https://api.example.com');

Constructor

new OpraHttpClient(serviceUrl: string, options?: FetchBackend.Options)
ParameterTypeDescription
serviceUrlstringBase URL of the OPRA service. Trailing slash is normalized.
optionsFetchBackend.OptionsOptional. Default headers, query params, and interceptors.
const client = new OpraHttpClient('https://api.example.com', {
defaults: {
headers: new Headers({ Authorization: 'Bearer eyJ...' }),
params: new URLSearchParams({ version: '2' }),
},
});

Properties

PropertyTypeDescription
serviceUrlstringResolved base URL (trailing slash normalized).
defaultsFetchBackend.RequestDefaultsDefault headers and query parameters applied to every request.

Request methods

Every request method returns an HttpRequestObservable — a lazy builder that only sends the request when you subscribe or call .getBody() / .getResponse().

get<TBody>(path, options?)

get<TBody = any>(
path: string,
options?: Omit<RequestOptions, 'method' | 'body'>,
): HttpRequestObservable<TBody>
const orders = await client.get<Order[]>('orders').getBody();

// With query parameters
const active = await client
.get<Order[]>('orders')
.param('filter', 'status = "active"')
.param('limit', 20)
.getBody();

post<TBody>(path, body, options?)

post<TBody = any>(path: string, body: any, options?: RequestOptions): HttpRequestObservable<TBody>
const order = await client.post<Order>('orders', {
productId: 'p1',
quantity: 2,
}).getBody();

patch<TBody>(path, body, options?)

patch<TBody = any>(path: string, body: any, options?: RequestOptions): HttpRequestObservable<TBody>
await client.patch('orders/123', { status: 'shipped' }).getBody();

put<TBody>(path, body, options?)

put<TBody = any>(path: string, body: any, options?: RequestOptions): HttpRequestObservable<TBody>
await client.put('orders/123', replacementPayload).getBody();

delete<TBody>(path, options?)

delete<TBody = any>(
path: string,
options?: Omit<RequestOptions, 'method' | 'body'>,
): HttpRequestObservable<TBody>
await client.delete('orders/123').getBody();

request<TBody>(path, options?)

Low-level method — pass any HTTP method via options.method. Used when a shorthand doesn't exist for the verb you need.

request<TBody = any>(
path: string,
options?: RequestOptions,
): HttpRequestObservable<TBody>
const result = await client.request('reports/export', {
method: 'POST',
body: { format: 'csv', dateRange: '2024-Q4' },
}).getBody<ReportFile>();

fetchDocument(options?)

Fetches the OPRA API schema from /$schema and returns a parsed ApiDocument. Referenced sub-documents are resolved recursively.

fetchDocument(options?: { documentId?: string }): Promise<ApiDocument>
const doc = await client.fetchDocument();
console.log(doc.controllers); // all registered controllers

Batch requests

bundle(requests)

Groups multiple HttpRequestObservable instances into a single POST /$bundle request with Content-Type: multipart/mixed. Returns an HttpBundleObservable.

Requests bound to the bundle do not fire individual network calls. After the bundle resolves, each request can still be awaited via .getBody() / .getResponse() — served from a local cache.

const r1 = client.get<Customer>('customers@1');
const r2 = client.get<Order[]>('orders');

const [res1, res2] = await client.bundle([r1, r2]).getResponses();

// Individual awaits also work after the bundle completes:
const customer = await r1.getBody();
const orders = await r2.getBody();

HttpBundleObservable


transaction(requests)

Same as bundle(), but appends ?transaction=true. The server wraps all sub-requests in a single database transaction — commits on full success, rolls back on any failure.

const r1 = client.post('orders', { productId: 'p1', qty: 2 });
const r2 = client.patch('inventory/p1', { reserve: 2 });

// Both commit together — or both roll back.
await client.transaction([r1, r2]).getResponses();

Transaction support is provided out of the box by @opra/mongodb and @opra/sqb.

HttpBundleObservable · Transactions


Observable vs Promise

Every HTTP method returns an HttpRequestObservable, which is both an RxJS Observable and a Promise-like object. Choose whichever fits your codebase:

Promise (async/await)

.getBody() and .getResponse() are the Promise shortcuts. Best for one-shot requests.

// Body only
const orders = await client.get<Order[]>('orders').getBody();

// Full response — status, headers, and body
const res = await client.post<Order>('orders', payload).getResponse();
if (res.ok) console.log(res.status, res.body);

Observable (RxJS)

HttpRequestObservable extends RxJS Observable. Pipe it through any operator — useful in reactive contexts such as Angular AsyncPipe, combining streams, or cancellation via takeUntil.

import { switchMap } from 'rxjs';

userId$.pipe(
switchMap(id => client.get<User>(`users/${id}`)),
).subscribe(user => console.log(user));
// Angular template — no subscription management needed
orders$ = client.get<Order[]>('orders');
// <li *ngFor="let o of orders$ | async">

Progress events

Upload and download progress is only available through the Observable path:

import { filter } from 'rxjs';
import { HttpObserveType, HttpEventType } from '@opra/client';

client.post('upload', file)
.options({ reportProgress: true })
.observe(HttpObserveType.Events)
.pipe(filter(e => e.type === HttpEventType.UploadProgress))
.subscribe(e => console.log(`${e.loaded} / ${e.total ?? '?'} bytes`));

Headers and parameters

Chain .header() and .param() on any request to add per-request headers or query parameters without affecting the client defaults.

const res = await client
.get<Report>('reports/monthly')
.header('X-Tenant-Id', tenantId)
.header('Accept-Language', 'tr')
.param('month', '2024-12')
.param('format', 'json')
.getBody();

Pass a headers object to set multiple at once:

client.get('orders').header({ 'X-Tenant-Id': tid, 'X-Trace-Id': traceId });

HttpRequestObservable


Error handling

Failed requests (4xx / 5xx) throw a ClientError with a structured issues array matching the OPRA error format:

import { ClientError } from '@opra/client';

try {
await client.get<Order>('orders/999').getBody();
} catch (err) {
if (err instanceof ClientError) {
console.log(err.status); // 404
console.log(err.issues); // [{ message: 'Not found', code: 'NOT_FOUND' }]
}
}

Network errors and timeouts are also thrown as ClientError with status: 0.

ClientError


Default headers and params

Set headers and query parameters that apply to every request through the options.defaults constructor argument or by mutating client.defaults after construction:

// Constructor
const client = new OpraHttpClient('https://api.example.com', {
defaults: {
headers: new Headers({ Authorization: `Bearer ${token}` }),
},
});

// Post-construction
client.defaults.headers.set('X-Tenant-Id', tenantId);

Per-request .header() and .param() calls are merged on top of the defaults — they do not replace them.


Interceptors

Interceptors let you inspect and modify every outgoing request and incoming response at the transport layer. Pass them through options.interceptors:

import { HttpInterceptor, HttpHandler } from '@opra/client';

class AuthInterceptor implements HttpInterceptor {
async intercept(req: Request, next: HttpHandler): Promise<Response> {
req.headers.set('Authorization', `Bearer ${await getToken()}`);
const res = await next.handle(req);
if (res.status === 401) await refreshToken();
return res;
}
}

const client = new OpraHttpClient('https://api.example.com', {
interceptors: [new AuthInterceptor()],
});

HttpInterceptor


See also