Code Examples
Express 4 with CurrencyCore
A shared fetch helper, timeout, error middleware, and in-memory cache for a Node.js Express 4 route.
An Express route should not know how to call CurrencyCore. Put the base URL, key, timeout, and error translation in one helper, then let routes ask for a snapshot. Errors thrown by that helper reach a single middleware that returns the upstream status and the API error envelope unchanged.
The example targets Express 4 on Node.js 18 or later, which provides global fetch and AbortSignal.timeout. Free includes 1,000 requests per month at 60 per minute. Test keys use the same reference data and quota and are capped at 60 per minute. Read the key from CURRENCYCORE_API_KEY.
// server.js
import express from "express";
const BASE = "https://api.currency-core.com/v1";
const KEY = process.env.CURRENCYCORE_API_KEY;
if (!KEY) throw new Error("Set CURRENCYCORE_API_KEY");
class UpstreamError extends Error {
constructor(status, code, message, details) {
super(message);
this.status = status;
this.code = code;
this.details = details;
}
}
async function ccGet(path, params = {}) {
const url = new URL(BASE + path);
for (const [name, value] of Object.entries(params)) {
if (value !== undefined && value !== "") {
url.searchParams.set(name, String(value));
}
}
let res;
try {
res = await fetch(url, {
headers: { Authorization: `Bearer ${KEY}`, Accept: "application/json" },
signal: AbortSignal.timeout(10_000),
});
} catch {
throw new UpstreamError(502, "internal_error", "Upstream request failed");
}
let body;
try {
body = await res.json();
} catch {
throw new UpstreamError(502, "internal_error", `Invalid JSON (HTTP ${res.status})`);
}
if (!res.ok) {
const err = body?.error ?? {};
throw new UpstreamError(
res.status,
err.code ?? "internal_error",
err.message ?? `HTTP ${res.status}`,
err.details,
);
}
return body;
}
const cache = new Map();
const today = () => new Date().toISOString().slice(0, 10);
async function getSnapshot(base, date) {
const id = `${base}:${date || "latest"}`;
const hit = cache.get(id);
if (hit && hit.expiresAt > Date.now()) return hit.body;
const body = await ccGet(`/rates/${encodeURIComponent(base)}`, date ? { date } : {});
const ttl = date && date < today() ? 86_400_000 : 300_000;
cache.set(id, { body, expiresAt: Date.now() + ttl });
return body;
}
const wrap = (fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);
const app = express();
app.get("/api/rates/:base", wrap(async (req, res) => {
const base = req.params.base.toUpperCase();
const date = String(req.query.date ?? "");
if (!/^[A-Z]{3}$/.test(base) || (date && !/^\d{4}-\d{2}-\d{2}$/.test(date))) {
throw new UpstreamError(400, "invalid_input", "Invalid base or date");
}
res.json(await getSnapshot(base, date));
}));
app.use((err, req, res, next) => {
if (!(err instanceof UpstreamError)) {
console.error(err);
return res.status(500).json({
error: { code: "internal_error", message: "Unexpected server error" },
});
}
const error = { code: err.code, message: err.message };
if (err.details) error.details = err.details;
res.status(err.status).json({ error });
});
app.listen(3000);
A successful call returns the dated map:
{
"base": "EUR",
"date": "2026-11-10",
"rates": { "EUR": 1, "USD": 1.0861, "INR": 91.62 }
}
A 403 for a date outside the Free 30-day window is forwarded as 403 with error.code set to forbidden, so the caller can branch without parsing prose.
Express 5 handles rejected promises from async handlers automatically and forwards them to error middleware. Express 4 does not, which is why the route uses wrap. A bare async handler that throws will otherwise leave the request hanging.
Production notes
- The
Mapis per process. With several instances or a restart, each one keeps its own copy. Use Redis when the cache must be shared. - Historical dates are stable for normal caching, but the
latestkey changes. Keep its TTL short. - Bound the cache or use an LRU package. An unbounded map is a memory leak on a public route.
- Do not log the
Authorizationheader or echo the key in error responses.
Next steps
- API reference and authentication
- Error codes and rate limits
- Caching guide for dated snapshots
- All developer guides