Skip to main content

HTTP Client

@opra/client works in any JavaScript environment — browser, Node.js, React, Vue, or plain TypeScript. It has no framework dependencies and uses the fetch API as its default transport.

npm install @opra/client

Setup

Create a single client instance and share it across your application.

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

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

Set default headers or query parameters applied to every request:

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

Making requests

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

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

// POST
const created = await client.post<Order>('orders', { productId: 'p1', qty: 2 }).getBody();

// PATCH
await client.patch('orders/123', { status: 'shipped' }).getBody();

// DELETE
await client.delete('orders/123').getBody();

Reading the full response

Use .getResponse() when you need the HTTP status or response headers in addition to the body:

const res = await client.post<Order>('orders', input).getResponse();

if (res.ok) {
console.log('Created:', res.body);
console.log('Location:', res.headers.get('Location'));
}

React

In React, use useState + useEffect for simple cases, or pair with TanStack Query for caching and loading states.

With useEffect

function OrderList() {
const [orders, setOrders] = useState<Order[]>([]);

useEffect(() => {
client.get<Order[]>('orders').getBody().then(setOrders);
}, []);

return <ul>{orders.map(o => <li key={o.id}>{o.name}</li>)}</ul>;
}

With TanStack Query

import { useQuery, useMutation } from '@tanstack/react-query';

function useOrders() {
return useQuery({
queryKey: ['orders'],
queryFn: () => client.get<Order[]>('orders').getBody(),
});
}

function useCreateOrder() {
return useMutation({
mutationFn: (input: OrderInput) =>
client.post<Order>('orders', input).getBody(),
});
}

Vue

In Vue 3, use ref + onMounted, or integrate with Pinia for shared state.

With Composition API

// composables/useOrders.ts
import { ref, onMounted } from 'vue';

export function useOrders() {
const orders = ref<Order[]>([]);
const loading = ref(false);

async function fetchOrders() {
loading.value = true;
orders.value = await client.get<Order[]>('orders').getBody();
loading.value = false;
}

onMounted(fetchOrders);
return { orders, loading, fetchOrders };
}

With Pinia

// stores/orders.ts
import { defineStore } from 'pinia';

export const useOrdersStore = defineStore('orders', {
state: () => ({ items: [] as Order[] }),
actions: {
async fetchAll() {
this.items = await client.get<Order[]>('orders').getBody();
},
async create(input: OrderInput) {
const order = await client.post<Order>('orders', input).getBody();
this.items.push(order);
},
},
});

Node.js

@opra/client runs in Node.js 18+ without any polyfills since fetch is available natively.

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

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

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

Error handling

All 4xx and 5xx responses throw a ClientError:

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

try {
await client.get<Order>('orders/not-found').getBody();
} catch (err) {
if (err instanceof ClientError) {
console.error(err.status); // 404
console.error(err.issues); // structured error details
}
}