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
}
}