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)
| Parameter | Type | Description |
|---|---|---|
serviceUrl | string | Base URL of the OPRA service. Trailing slash is normalized. |
options | FetchBackend.Options | Optional. 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
| Property | Type | Description |
|---|---|---|
serviceUrl | string | Resolved base URL (trailing slash normalized). |
defaults | FetchBackend.RequestDefaults | Default 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();
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 });
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.
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()],
});
See also
HttpClientBase— abstract base with all HTTP and bundle methodsHttpRequestObservable— fluent builder returned by every request methodHttpBundleObservable— returned bybundle()andtransaction()FetchBackend— backend options and customizationHttpInterceptor— request/response middlewareClientError— error type thrown on failed requests