|
| 1 | +# @unirate/express |
| 2 | + |
| 3 | +[Express](https://expressjs.com) middleware and router for the [UniRate API](https://unirateapi.com) — free currency exchange rates, conversion, and VAT rates. |
| 4 | + |
| 5 | +Attach a typed UniRate client to every request, or mount a ready-made set of JSON endpoints, and keep your API key server-side. Works with Express 4 and 5. |
| 6 | + |
| 7 | +## Features |
| 8 | + |
| 9 | +- `unirateClient()` middleware — attaches a typed `UniRateClient` to `req.unirate` |
| 10 | +- `unirate()` — a mountable Express `Router` with `/rate`, `/convert`, `/currencies`, `/vat` routes |
| 11 | +- Full error mapping — UniRate statuses (400 / 401 / 403 / 404 / 429 / 503) mirrored to proper HTTP responses; transport failures become 502; a missing key becomes 500 |
| 12 | +- Currency-code validation + uppercasing |
| 13 | +- API key read from options or the `UNIRATE_API_KEY` environment variable |
| 14 | +- Zero runtime dependencies (uses native `fetch`); `express` is a peer dependency |
| 15 | +- TypeScript throughout — `req.unirate` is typed via Express module augmentation |
| 16 | +- ESM + CJS builds |
| 17 | + |
| 18 | +## Install |
| 19 | + |
| 20 | +```bash |
| 21 | +npm install @unirate/express express |
| 22 | +``` |
| 23 | + |
| 24 | +`express` is a peer dependency (`>=4.0.0`, works with v4 and v5). Requires Node 18+ for native `fetch`. Get a free UniRate API key at [unirateapi.com](https://unirateapi.com). |
| 25 | + |
| 26 | +## Quick start |
| 27 | + |
| 28 | +### Middleware |
| 29 | + |
| 30 | +```ts |
| 31 | +import express from "express"; |
| 32 | +import { unirateClient } from "@unirate/express"; |
| 33 | + |
| 34 | +const app = express(); |
| 35 | +app.use(unirateClient()); // reads process.env.UNIRATE_API_KEY |
| 36 | + |
| 37 | +app.get("/eur", async (req, res) => { |
| 38 | + const rate = await req.unirate.getRate("USD", "EUR"); |
| 39 | + res.json({ pair: "USD/EUR", rate }); |
| 40 | +}); |
| 41 | + |
| 42 | +app.listen(3000); |
| 43 | +``` |
| 44 | + |
| 45 | +`req.unirate` is fully typed via Express's `Request` interface — no casting needed. |
| 46 | + |
| 47 | +### Router |
| 48 | + |
| 49 | +```ts |
| 50 | +import express from "express"; |
| 51 | +import { unirate } from "@unirate/express"; |
| 52 | + |
| 53 | +const app = express(); |
| 54 | +app.use("/api/unirate", unirate()); |
| 55 | +``` |
| 56 | + |
| 57 | +That mounts: |
| 58 | + |
| 59 | +``` |
| 60 | +GET /api/unirate/rate?from=USD&to=EUR |
| 61 | +GET /api/unirate/convert?from=USD&to=EUR&amount=100 |
| 62 | +GET /api/unirate/currencies |
| 63 | +GET /api/unirate/vat?country=DE |
| 64 | +``` |
| 65 | + |
| 66 | +## Options |
| 67 | + |
| 68 | +Both `unirateClient()` and `unirate()` accept the same options: |
| 69 | + |
| 70 | +| Option | Type | Default | Description | |
| 71 | +|---|---|---|---| |
| 72 | +| `apiKey` | `string` | — | API key. If omitted, resolved from the env variable | |
| 73 | +| `envKey` | `string` | `"UNIRATE_API_KEY"` | Name of the env variable to read the key from | |
| 74 | +| `baseUrl` | `string` | `https://api.unirateapi.com` | Override the API base URL | |
| 75 | +| `fetch` | `typeof fetch` | `globalThis.fetch` | Inject a custom `fetch` | |
| 76 | +| `timeoutMs` | `number` | `30000` | Request timeout | |
| 77 | +| `userAgent` | `string` | `@unirate/express/<version>` | Custom User-Agent header | |
| 78 | + |
| 79 | +```ts |
| 80 | +app.use(unirateClient({ apiKey: process.env.UNIRATE_API_KEY })); |
| 81 | +// or a custom env variable name |
| 82 | +app.use("/fx", unirate({ envKey: "MY_UNIRATE_KEY" })); |
| 83 | +``` |
| 84 | + |
| 85 | +## Routes |
| 86 | + |
| 87 | +### `GET /rate` |
| 88 | + |
| 89 | +| Param | Required | Default | Description | |
| 90 | +|---|---|---|---| |
| 91 | +| `from` | no | `USD` | Source currency (3-letter ISO code) | |
| 92 | +| `to` | no | — | Target currency. Omit to return all rates for `from` | |
| 93 | + |
| 94 | +```jsonc |
| 95 | +// /rate?from=USD&to=EUR → |
| 96 | +{ "rate": 0.92 } |
| 97 | +// /rate?from=USD → |
| 98 | +{ "rates": { "EUR": 0.92, "GBP": 0.79 } } |
| 99 | +``` |
| 100 | + |
| 101 | +### `GET /convert` |
| 102 | + |
| 103 | +| Param | Required | Default | Description | |
| 104 | +|---|---|---|---| |
| 105 | +| `from` | no | `USD` | Source currency | |
| 106 | +| `to` | yes | — | Target currency | |
| 107 | +| `amount` | no | `1` | Amount to convert (positive number) | |
| 108 | + |
| 109 | +```jsonc |
| 110 | +// /convert?from=USD&to=EUR&amount=100 → |
| 111 | +{ "result": 92.5 } |
| 112 | +``` |
| 113 | + |
| 114 | +### `GET /currencies` |
| 115 | + |
| 116 | +```jsonc |
| 117 | +{ "currencies": ["USD", "EUR", "GBP", "..."] } |
| 118 | +``` |
| 119 | + |
| 120 | +### `GET /vat` |
| 121 | + |
| 122 | +| Param | Required | Default | Description | |
| 123 | +|---|---|---|---| |
| 124 | +| `country` | no | — | ISO-3166 alpha-2 code. Omit for all countries | |
| 125 | + |
| 126 | +```jsonc |
| 127 | +// /vat?country=DE → |
| 128 | +{ "country": "DE", "vat_data": { "country_code": "DE", "country_name": "Germany", "vat_rate": 19 } } |
| 129 | +``` |
| 130 | + |
| 131 | +## Using the client directly |
| 132 | + |
| 133 | +The internal client is exported for direct use in your own handlers: |
| 134 | + |
| 135 | +```ts |
| 136 | +import { UniRateClient, RateLimitError } from "@unirate/express"; |
| 137 | + |
| 138 | +const client = new UniRateClient({ apiKey: "..." }); |
| 139 | + |
| 140 | +try { |
| 141 | + const rate = await client.getRate("USD", "EUR"); // number |
| 142 | + const all = await client.getRate("USD"); // Record<string, number> |
| 143 | + const eur = await client.convert("EUR", 100, "USD"); // number |
| 144 | + const codes = await client.getSupportedCurrencies(); // string[] |
| 145 | + const vat = await client.getVatRate("DE"); // { country, vat_data } |
| 146 | +} catch (err) { |
| 147 | + if (err instanceof RateLimitError) { |
| 148 | + // back off and retry |
| 149 | + } |
| 150 | +} |
| 151 | +``` |
| 152 | + |
| 153 | +It is also available on the `@unirate/express/client` subpath if you want the client without pulling in the Express glue. |
| 154 | + |
| 155 | +## Error handling |
| 156 | + |
| 157 | +The router maps thrown errors to HTTP responses with a `{ "error": string }` body: |
| 158 | + |
| 159 | +| Status | Meaning | |
| 160 | +|---|---| |
| 161 | +| `400` | Invalid parameter (bad currency/country code, missing/invalid amount) | |
| 162 | +| `401` | Missing or invalid API key | |
| 163 | +| `403` | Endpoint requires a Pro subscription | |
| 164 | +| `404` | Currency not found or no data available | |
| 165 | +| `429` | Rate limit exceeded | |
| 166 | +| `500` | API key not configured on the server | |
| 167 | +| `502` | UniRate upstream / transport error | |
| 168 | +| `503` | Service unavailable | |
| 169 | + |
| 170 | +When using the client directly, these map to typed error classes — all extending `UniRateError`: |
| 171 | +`InvalidRequestError`, `AuthenticationError`, `ProRequiredError`, `InvalidCurrencyError`, `RateLimitError`, `ServiceUnavailableError`. |
| 172 | + |
| 173 | +## Free vs Pro tier |
| 174 | + |
| 175 | +The free tier covers `/rate`, `/convert`, `/currencies`, and `/vat`. Historical and time-series endpoints require a [Pro subscription](https://unirateapi.com/pricing) and are not exposed by this package. |
| 176 | + |
| 177 | +## Example |
| 178 | + |
| 179 | +A runnable Express app lives in [`examples/server.mjs`](./examples/server.mjs): |
| 180 | + |
| 181 | +```bash |
| 182 | +npm run build |
| 183 | +UNIRATE_API_KEY=your-key node examples/server.mjs |
| 184 | +``` |
| 185 | + |
| 186 | +## Related packages |
| 187 | + |
| 188 | +**UniRate API client libraries:** [Python](https://github.com/UniRate-API/unirate-api-python) · [Node.js](https://github.com/UniRate-API/unirate-api-nodejs) · [Go](https://github.com/UniRate-API/unirate-api-go) · [Rust](https://github.com/UniRate-API/unirate-api-rust) · [Ruby](https://github.com/UniRate-API/unirate-api-ruby) · [PHP](https://github.com/UniRate-API/unirate-api-php) · [Java](https://github.com/UniRate-API/unirate-api-java) · [Swift](https://github.com/UniRate-API/unirate-api-swift) · [.NET](https://github.com/UniRate-API/unirate-api-dotnet) |
| 189 | + |
| 190 | +**Framework integrations:** [Fastify](https://github.com/UniRate-API/fastify-unirate) · [Hono](https://github.com/UniRate-API/hono-unirate) · [Next.js](https://github.com/UniRate-API/next-unirate) · [Nuxt](https://github.com/UniRate-API/nuxt-unirate) · [SvelteKit](https://github.com/UniRate-API/sveltekit-unirate) · [Astro](https://github.com/UniRate-API/astro-unirate) · [NestJS](https://github.com/UniRate-API/nestjs-unirate) · [Remix](https://github.com/UniRate-API/remix-unirate) · [Angular](https://github.com/UniRate-API/angular-unirate) · [Vue](https://github.com/UniRate-API/vue-unirate) · [React](https://github.com/UniRate-API/react-unirate) |
| 191 | + |
| 192 | +**Platform:** [Cloudflare Workers](https://github.com/UniRate-API/cloudflare-workers-unirate) · [MCP server](https://github.com/UniRate-API/unirate-mcp) · [CLI](https://github.com/UniRate-API/unirate-cli) |
| 193 | + |
| 194 | +## License |
| 195 | + |
| 196 | +MIT © Unirate Team |
0 commit comments