@unirate/qwik
v0.1.0
Published
Qwik & Qwik City integration for the UniRate currency-exchange API. Server-safe client factory, routeLoader$-ready loader factories, a server$-friendly data API, a useUniRate helper, and a key-hiding proxy handler. Zero runtime deps.
Maintainers
Readme
@unirate/qwik
Qwik & Qwik City integration for the UniRate currency-exchange API.
- Server-safe client factory —
createUniRate()resolves the key fromUNIRATE_API_KEYat call time. Use it insiderouteLoader$,routeAction$, orserver$, so the key never enters the browser bundle. routeLoader$-ready loader factories —createRateLoader,createConvertLoader,createCurrenciesLoader. Drop them straight intorouteLoader$(...).server$-friendly data functions —getRate,convert,listCurrencies,getVatRates,getHistoricalRate,getTimeSeries,getHistoricalLimits.useUniRate(signal)— adapts a loader signal into a tidy{ data, error }view.- Key-hiding proxy —
createUniRateProxyHandler()for a/api/unirateendpoint that forwards allow-listed paths with the key injected server-side. - Zero runtime deps. Native
fetch. Full typed error mapping.
Install
npm install @unirate/qwik
# or
pnpm add @unirate/qwik
# or
yarn add @unirate/qwikPeer deps: @builder.io/qwik ^1.5 and @builder.io/qwik-city ^1.5 (both
optional — you can use the raw client and the proxy handler without Qwik
City). Requires Node 18.17+.
Get a free API key at unirateapi.com — the free tier covers latest rates, conversions, currency lists, and VAT rates for ~600 currencies including crypto. Historical rates, time-series, and commodity feeds require Pro.
Why no Qwik build step? This package ships as plain,
tsc-compiled TypeScript utilities. It never calls$itself, so it does not need the Qwik optimizer to build or publish. You wrap the factories in your ownrouteLoader$/server$, where your app's optimizer takes over. That keeps the dependency surface tiny and the package trivially testable.
Quickstart
Set UNIRATE_API_KEY in your Qwik City .env, then in
src/routes/currency/index.tsx:
import { component$ } from "@builder.io/qwik";
import { routeLoader$ } from "@builder.io/qwik-city";
import { createRateLoader, useUniRate } from "@unirate/qwik";
// Runs only on the server; reads UNIRATE_API_KEY from the platform env.
export const useRate = routeLoader$(createRateLoader({ defaultFrom: "USD" }));
export default component$(() => {
// Visit /currency?from=USD&to=EUR
const { data, error } = useUniRate(useRate());
if (error) return <p>{error.message}</p>;
return (
<p>
{data?.from} → {data?.to}: {JSON.stringify(data?.rate)}
</p>
);
});A full route (all three loaders + server$) is in
examples/routes-currency-index.tsx, and
the proxy endpoint in
examples/routes-api-unirate-index.ts.
Loader factories
Each factory returns an async (ev) => data function shaped for
routeLoader$. It resolves the API key in this order: explicit apiKey
option → ev.env.get("UNIRATE_API_KEY") → process.env.UNIRATE_API_KEY.
import {
createRateLoader,
createConvertLoader,
createCurrenciesLoader,
} from "@unirate/qwik";
export const useRate = routeLoader$(createRateLoader({ defaultFrom: "USD" }));
export const useConvert = routeLoader$(createConvertLoader());
export const useCurrencies = routeLoader$(createCurrenciesLoader());| Factory | Reads query params | Returns |
|---|---|---|
| createRateLoader | from (default USD), to | { rate, from, to } |
| createConvertLoader | from, to (required), amount (default 1) | { result, from, to, amount } |
| createCurrenciesLoader | — | { currencies } |
createConvertLoader throws a Response with status 400 when to is
missing, which Qwik City surfaces as an error boundary.
Options for every factory:
{
apiKey?: string; // overrides env resolution
baseUrl?: string; // point at a proxy
timeoutMs?: number; // default 30_000
defaultFrom?: string; // rate/convert base when `from` is absent (default USD)
}useUniRate(signal)
A tiny, optimizer-free adapter. Hand it any Qwik Signal-like value (e.g. the
result of a routeLoader$) and it returns { data, error }. If the payload is
an { error: string } envelope, it comes back as a normalized Error;
otherwise it passes through on data.
const rate = useUniRate(useRate());
if (rate.error) return <p>{rate.error.message}</p>;
return <p>{JSON.stringify(rate.data?.rate)}</p>;server$ data functions & the client factory
createUniRate() gives you the full UniRate surface with Qwik-friendly
(from, to, amount) argument order plus the raw client:
import { server$ } from "@builder.io/qwik-city";
import { createUniRate } from "@unirate/qwik";
export const convertMoney = server$(async function (from: string, to: string, amount: number) {
const unirate = createUniRate(); // key from server env
return unirate.convert(from, to, amount); // → number
});Available methods on the object returned by createUniRate():
getRate(from, to?)→number(withto) orRecord<string, number>convert(from, to, amount)→numberlistCurrencies()→string[]getVatRates(country?)→ all countries, or one whencountryis passedgetHistoricalRate(date, from, to?, amount?)→numberor map (Pro)getTimeSeries(startDate, endDate, base?, currencies?, amount?)→ nested map (Pro)getHistoricalLimits()→ limits struct (Pro)client— the underlyingUniRateClient
Module-level getRate / convert / listCurrencies / … are also exported;
they lazily build a default instance from process.env.UNIRATE_API_KEY, handy
for wrapping directly in server$.
Key-hiding proxy endpoint
For client-side fetches that must not leak the key, mount a proxy route
(src/routes/api/unirate/index.ts):
import type { RequestHandler } from "@builder.io/qwik-city";
import { createUniRateProxyHandler } from "@unirate/qwik";
const proxy = createUniRateProxyHandler();
export const onGet: RequestHandler = async (ev) => {
const res = await proxy(new URL(ev.url), ev.env.get("UNIRATE_API_KEY"));
const ct = res.headers.get("Content-Type");
if (ct) ev.headers.set("Content-Type", ct);
ev.send(res);
};The browser then calls
/api/unirate?path=/api/rates&from=USD&to=EUR. Only these paths are allowed by
default (override with allowedPaths): /api/rates, /api/convert,
/api/currencies, /api/vat/rates, /api/historical/rates,
/api/historical/timeseries, /api/historical/limits. Responses carry a
Cache-Control: public, s-maxage=60, stale-while-revalidate=300 header.
Using the raw client
UniRateClient is exported from @unirate/qwik and @unirate/qwik/client. It
takes a fetch option for injection (that's how the test suite avoids the
network):
import { UniRateClient } from "@unirate/qwik/client";
const client = new UniRateClient({ apiKey: process.env.UNIRATE_API_KEY! });
const rate = await client.getRate("USD", "EUR"); // 0.92Errors
Every failure is a typed subclass of UniRateError, exported for instanceof
checks:
AuthenticationError— 401, bad / missing keyRateLimitError— 429InvalidCurrencyError— 404, unknown currency codeInvalidRequestError— 400, bad paramsProRequiredError— 403, endpoint requires a Pro subscriptionUniRateError— base class for any other failure (carriesstatusandbody)
import { InvalidCurrencyError } from "@unirate/qwik";
try {
await unirate.getRate("USD", "XYZ");
} catch (err) {
if (err instanceof InvalidCurrencyError) {
// unknown currency
}
}Rate limits
The free tier is rate-limited per key; on 429 you'll get a RateLimitError.
Keep the key server-side (loaders, server$, or the proxy handler) so it is
never exposed to end users.
Other UniRate clients
UniRate ships official client libraries and framework integrations across the ecosystem. The repos below are all maintained under the UniRate-API org.
- Languages: Python · Node.js / TypeScript · Go · Rust · Java · Ruby · PHP · .NET · Swift
- Web frameworks: React · Remix · NestJS · Django / Wagtail · FastAPI · Flask · tRPC
- Static-site generators: Astro · Eleventy · Hugo
- Data / orchestration: Airflow · dbt · LangChain
- Workflow / no-code: n8n · Google Sheets · MCP server
Get a free API key at unirateapi.com.
License
MIT © Unirate Team
