Skip to content

About

Qwik & Qwik City integration for the UniRate currency-exchange API. Zero runtime deps.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

@unirate/qwik

npm ci License: MIT

Qwik & Qwik City integration for the UniRate currency-exchange API.

  • Server-safe client factory — createUniRate() resolves the key from UNIRATE_API_KEY at call time. Use it inside routeLoader$, routeAction$, or server$, so the key never enters the browser bundle.
  • routeLoader$-ready loader factories — createRateLoader, createConvertLoader, createCurrenciesLoader. Drop them straight into routeLoader$(...).
  • 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/unirate endpoint 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/qwik

Peer 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 own routeLoader$ / 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 (with to) or Record<string, number>
  • convert(from, to, amount) → number
  • listCurrencies() → string[]
  • getVatRates(country?) → all countries, or one when country is passed
  • getHistoricalRate(date, from, to?, amount?) → number or map (Pro)
  • getTimeSeries(startDate, endDate, base?, currencies?, amount?) → nested map (Pro)
  • getHistoricalLimits() → limits struct (Pro)
  • client — the underlying UniRateClient

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.92

Errors

Every failure is a typed subclass of UniRateError, exported for instanceof checks:

  • AuthenticationError — 401, bad / missing key
  • RateLimitError — 429
  • InvalidCurrencyError — 404, unknown currency code
  • InvalidRequestError — 400, bad params
  • ProRequiredError — 403, endpoint requires a Pro subscription
  • UniRateError — base class for any other failure (carries status and body)
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.

Get a free API key at unirateapi.com.

License

MIT © Unirate Team

About

Qwik & Qwik City integration for the UniRate currency-exchange API. Zero runtime deps.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages