Client & CLI
OPRA provides a typed HTTP client for consuming your API, and a CLI tool that generates client code automatically from the live API schema. Together they close the loop between server and client — changes on the server surface as TypeScript compile errors on the client.
CLI — generating client code
oprimp connects to a running OPRA service, reads the /$schema endpoint, and generates typed TypeScript models and controller proxies:
npx oprimp https://api.example.com ./src/generated
The generated output contains:
- Model types — TypeScript interfaces and classes for every data type in the API
- Controller proxies — Typed wrappers for each HTTP controller so you call methods instead of constructing URLs manually
Re-run oprimp whenever the API changes. If the server renames a field or drops a parameter, the generated types change and any code that relied on the old shape fails at compile time — not at runtime.
# In CI — keep client in sync with the deployed service
npx oprimp https://api.example.com ./src/generated
HTTP Client
@opra/client is a lightweight HTTP client that works in any JavaScript environment — browser, Node.js, React, Vue, or plain TypeScript. No framework dependencies.
import { OpraHttpClient } from '@opra/client';
const client = new OpraHttpClient('https://api.example.com');
// GET
const orders = await client.get<Order[]>('orders').getBody();
// POST
const created = await client.post<Order>('orders', { productId: 'p1' }).getBody();
// PATCH
await client.patch('orders/123', { status: 'shipped' }).getBody();
// DELETE
await client.delete('orders/123').getBody();
Every request returns an HttpRequestObservable — you can await it like a Promise or pipe it through RxJS operators:
// Promise
const order = await client.get<Order>('orders/123').getBody();
// Observable
userId$.pipe(
switchMap(id => client.get<User>(`users/${id}`))
).subscribe(user => ...);
Errors are normalized into ClientError with a status code and 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' }]
}
}
Angular
@opra/angular is a drop-in replacement that delegates HTTP transport to Angular's HttpClient, keeping Angular's interceptors, HttpContext, and DI system fully active:
import { OpraAngularClient } from '@opra/angular';
@Injectable({ providedIn: 'root' })
export class ApiService {
constructor(private client: OpraAngularClient) {}
getOrders() {
return this.client.get<Order[]>('orders').getBody();
}
}
Batch requests & transactions
Send multiple requests in a single HTTP round-trip using client.bundle(). The client serializes each request as a multipart/mixed part, sends them as one POST /$bundle, and distributes the sub-responses back:
const r1 = client.get<Customer>('customers@1');
const r2 = client.get<Order[]>('orders');
const [res1, res2] = await client.bundle([r1, r2]).getResponses();
// Individual requests can still be awaited — resolved from the bundle:
const customer = await r1.getBody();
const orders = await r2.getBody();
To wrap all sub-requests in a single database transaction, use client.transaction() — it appends ?transaction=true automatically. The server commits on full success or 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 — no server-side handler changes needed.