\n \n\nHTML\ncat > main.jsx <<'JSX'\nimport { createRoot } from \"react-dom/client\";\nimport GoldPrice from \"./GoldPrice.jsx\";\n\ncreateRoot(document.getElementById(\"root\")).render();\nJSX\n\n// Add the server route\nimport express from \"express\";\nimport { fileURLToPath } from \"node:url\";\n\nconst key = process.env.GOLDPRICE_API_KEY;\nif (!key) throw new Error(\"Set GOLDPRICE_API_KEY before starting the server\");\n\nconst app = express();\n\napp.get(\"/api/gold-price\", async (req, res) => {\n res.set(\"Cache-Control\", \"no-store\");\n try {\n const response = await fetch(\n \"https://api.goldprice.dev/v1/spot/XAU-USD-SPOT\",\n {\n headers: { Authorization: `Bearer ${key}` },\n signal: AbortSignal.timeout(10_000),\n }\n );\n\n if (response.status === 429) {\n res.set(\"Retry-After\", response.headers.get(\"Retry-After\") || \"60\");\n return res.status(429).json({ error: \"Too many requests. Please wait.\" });\n }\n if (response.status === 401) {\n return res.status(401).json({ error: \"Check GOLDPRICE_API_KEY on the server.\" });\n }\n if (!response.ok) {\n return res.status(502).json({ error: \"The gold price could not be loaded.\" });\n }\n\n const data = await response.json();\n if (\n typeof data.price !== \"string\" ||\n !/^\\d+(\\.\\d+)?$/.test(data.price) ||\n typeof data.computed_at !== \"string\" ||\n !Number.isFinite(Date.parse(data.computed_at)) ||\n typeof data.is_stale !== \"boolean\"\n ) {\n throw new Error(\"Invalid price response\");\n }\n\n res.json({\n price: data.price,\n computed_at: data.computed_at,\n is_stale: data.is_stale,\n });\n } catch {\n res.status(502).json({ error: \"The gold price is unavailable. Try again later.\" });\n }\n});\n\napp.use(express.static(fileURLToPath(new URL(\"./public/\", import.meta.url))));\napp.listen(3000, \"127.0.0.1\", () => {\n console.log(\"Open http://localhost:3000\");\n});\n\n// Add the gold price component\nimport { useEffect, useState } from \"react\";\n\nexport default function GoldPrice() {\n const [data, setData] = useState(null);\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState(\"\");\n\n useEffect(() => {\n let stopped = false;\n let pending = false;\n let controller;\n let retryAt = 0;\n\n async function refresh() {\n if (pending || Date.now() < retryAt) return;\n pending = true;\n setLoading(true);\n controller = new AbortController();\n const request = controller;\n const timeout = setTimeout(() => request.abort(), 15_000);\n\n try {\n const response = await fetch(\"/api/gold-price\", {\n signal: request.signal,\n cache: \"no-store\",\n });\n if (response.status === 429) {\n const header = response.headers.get(\"Retry-After\");\n const seconds = header === null ? NaN : Number(header);\n const deadline = Number.isFinite(seconds)\n ? Date.now() + Math.max(0, seconds) * 1000\n : Date.parse(header || \"\");\n retryAt = Math.max(\n Date.now() + 60_000,\n Number.isFinite(deadline) ? deadline : 0\n );\n }\n if (!response.ok) {\n const problem = await response.json();\n throw new Error(problem.error || \"Cannot load the gold price.\");\n }\n const next = await response.json();\n if (!stopped) {\n setData(next);\n setError(\"\");\n }\n } catch (err) {\n if (!stopped) {\n setError(err.name === \"AbortError\" ? \"The request timed out.\" : err.message);\n }\n } finally {\n clearTimeout(timeout);\n pending = false;\n if (!stopped) setLoading(false);\n }\n }\n\n refresh();\n const interval = setInterval(refresh, 60_000);\n return () => {\n stopped = true;\n clearInterval(interval);\n controller?.abort();\n };\n }, []);\n\n return (\n
\n

Gold price

\n {loading &&

Loading gold price...

}\n {error &&

{error}

}\n {data && (\n <>\n

USD {data.price} per troy ounce

\n

Price computed:

\n {data.is_stale &&

This price is marked stale. Check the update time.

}\n \n )}\n

Free plan prices are delayed by fifteen minutes.

\n

Price data: goldprice.dev

\n
\n );\n}\n\n// Build and open the page\nnpx esbuild main.jsx --bundle --jsx=automatic --minify --outfile=public/app.js\nnode server.js","isPartOf":{"@type":"TechArticle","@id":"https://goldprice.dev/tutorials/react"},"license":"https://goldprice.dev/terms"}

TutorialsCode

How to get live gold prices in Gold price in React

Show the gold price in React with an Express route, a private API key, status messages and automatic refresh.

23 Oct 2026 · 5 min read

To show the gold price in React, fetch /api/gold-price from a function component. Your Express server calls goldprice.dev at /v1/spot/XAU-USD-SPOT and keeps the API key in GOLDPRICE_API_KEY. Keep the returned price as a decimal string. Show its calculation time and stale flag, then refresh every sixty seconds. Free prices are delayed by fifteen minutes. The browser receives price data without the key.

Get your API key

Get a free key at goldprice.dev/onboarding. No credit card is required. Replace the placeholder below with your key, then run it in your terminal:

Shell
export GOLDPRICE_API_KEY=ga_live_replace_with_your_key

Keep this terminal open for the remaining steps. Never put the key in React code or a browser environment variable. Anyone who loads the page can inspect its JavaScript and network requests.

Create the React project

Use Node eighteen or newer with npm. Run these commands in Bash or zsh. They install React eighteen, Express and esbuild, then create the HTML page and React entry point.

The project uses ES modules. Save the next files in the gold-price-react folder created here.

shell
mkdir gold-price-reactcd gold-price-reactnpm init -ynpm pkg set type=modulenpm install react@18 react-dom@18 express@5npm install --save-dev esbuild@0.25mkdir publiccat > public/index.html <<'HTML'<!doctype html><html lang="en">  <head>    <meta charset="utf-8">    <meta name="viewport" content="width=device-width, initial-scale=1">    <title>Gold price in React</title>  </head>  <body>    <div id="root"></div>    <script src="/app.js" defer></script>  </body></html>HTMLcat > main.jsx <<'JSX'import { createRoot } from "react-dom/client";import GoldPrice from "./GoldPrice.jsx";createRoot(document.getElementById("root")).render(<GoldPrice />);JSX

Add the server route

Save this as server.js. The server reads GOLDPRICE_API_KEY, fetches the gold spot price and returns only price, computed_at and is_stale. The price stays a decimal string.

Express also serves the browser files, so /api/gold-price works without a separate proxy or CORS setup. Rate limit responses pass along Retry-After. Failed requests return an error message without exposing the key.

server.js
import express from "express";import { fileURLToPath } from "node:url";const key = process.env.GOLDPRICE_API_KEY;if (!key) throw new Error("Set GOLDPRICE_API_KEY before starting the server");const app = express();app.get("/api/gold-price", async (req, res) => {  res.set("Cache-Control", "no-store");  try {    const response = await fetch(      "https://api.goldprice.dev/v1/spot/XAU-USD-SPOT",      {        headers: { Authorization: `Bearer ${key}` },        signal: AbortSignal.timeout(10_000),      }    );    if (response.status === 429) {      res.set("Retry-After", response.headers.get("Retry-After") || "60");      return res.status(429).json({ error: "Too many requests. Please wait." });    }    if (response.status === 401) {      return res.status(401).json({ error: "Check GOLDPRICE_API_KEY on the server." });    }    if (!response.ok) {      return res.status(502).json({ error: "The gold price could not be loaded." });    }    const data = await response.json();    if (      typeof data.price !== "string" ||      !/^\d+(\.\d+)?$/.test(data.price) ||      typeof data.computed_at !== "string" ||      !Number.isFinite(Date.parse(data.computed_at)) ||      typeof data.is_stale !== "boolean"    ) {      throw new Error("Invalid price response");    }    res.json({      price: data.price,      computed_at: data.computed_at,      is_stale: data.is_stale,    });  } catch {    res.status(502).json({ error: "The gold price is unavailable. Try again later." });  }});app.use(express.static(fileURLToPath(new URL("./public/", import.meta.url))));app.listen(3000, "127.0.0.1", () => {  console.log("Open http://localhost:3000");});

Add the gold price component

Save this as GoldPrice.jsx. The effect fetches immediately and refreshes every sixty seconds. It shows loading and error messages, the price in US dollars per troy ounce, and the computed_at timestamp.

Cleanup clears the interval and cancels any pending request. A failed refresh keeps the previous price visible with an error. A stale response shows a warning. After a rate limit response, the component reads Retry-After and waits before requesting again.

The visible goldprice.dev link near the price meets the Free plan attribution requirement.

GoldPrice.jsx
import { useEffect, useState } from "react";export default function GoldPrice() {  const [data, setData] = useState(null);  const [loading, setLoading] = useState(true);  const [error, setError] = useState("");  useEffect(() => {    let stopped = false;    let pending = false;    let controller;    let retryAt = 0;    async function refresh() {      if (pending || Date.now() < retryAt) return;      pending = true;      setLoading(true);      controller = new AbortController();      const request = controller;      const timeout = setTimeout(() => request.abort(), 15_000);      try {        const response = await fetch("/api/gold-price", {          signal: request.signal,          cache: "no-store",        });        if (response.status === 429) {          const header = response.headers.get("Retry-After");          const seconds = header === null ? NaN : Number(header);          const deadline = Number.isFinite(seconds)            ? Date.now() + Math.max(0, seconds) * 1000            : Date.parse(header || "");          retryAt = Math.max(            Date.now() + 60_000,            Number.isFinite(deadline) ? deadline : 0          );        }        if (!response.ok) {          const problem = await response.json();          throw new Error(problem.error || "Cannot load the gold price.");        }        const next = await response.json();        if (!stopped) {          setData(next);          setError("");        }      } catch (err) {        if (!stopped) {          setError(err.name === "AbortError" ? "The request timed out." : err.message);        }      } finally {        clearTimeout(timeout);        pending = false;        if (!stopped) setLoading(false);      }    }    refresh();    const interval = setInterval(refresh, 60_000);    return () => {      stopped = true;      clearInterval(interval);      controller?.abort();    };  }, []);  return (    <section aria-label="Gold price">      <h2>Gold price</h2>      {loading && <p role="status">Loading gold price...</p>}      {error && <p role="alert">{error}</p>}      {data && (        <>          <p>USD {data.price} per troy ounce</p>          <p>Price computed: <time dateTime={data.computed_at}>{data.computed_at}</time></p>          {data.is_stale && <p role="status">This price is marked stale. Check the update time.</p>}        </>      )}      <p>Free plan prices are delayed by fifteen minutes.</p>      <p>Price data: <a href="https://goldprice.dev/docs/api-reference">goldprice.dev</a></p>    </section>  );}

Build and open the page

In the same terminal, from gold-price-react, run the commands below. Open http://localhost:3000 in your browser. Express serves the built React app and its API route from the same origin.

Rebuild after changing browser code. Restart the server after changing its code or API key. The server listens locally for this tutorial.

shell
npx esbuild main.jsx --bundle --jsx=automatic --minify --outfile=public/app.jsnode server.js

Expected output

JSON
{  "symbol": "XAU",  "quote_currency": "USD",  "unit": "troy_ounce",  "contract_type": "spot",  "price": "4023.54",  "is_stale": false,  "computed_at": "2026-10-23T10:51:32.965761+00:00"}

This is an example response from the goldprice.dev spot endpoint. Your server returns only price, computed_at and is_stale. React displays the price and calculation time, with a warning when is_stale is true. Actual values change.

Common errors

CodeWhat you seeFix
401The page says to check GOLDPRICE_API_KEY on the serverSet GOLDPRICE_API_KEY in the terminal that starts Express, using your actual key. Restart node server.js after changing it. Keep the key out of browser files.
429The page says there are too many requestsThe request reached a rate or quota limit. The component reads Retry-After and skips refreshes until that wait has passed. Reduce requests across tabs and visitors, or review plan limits. Do not retry in a tight loop.
502The page says the gold price is unavailableThe server could not fetch or validate a price response. Check its network access and try again later. The component keeps any previous price visible and shows an error so an unsuccessful refresh is clear.
N/AThe page is blank or the API route cannot be foundRun the build command before starting Express, then open the Express address shown in the terminal. Do not open the HTML file directly or use a separate frontend server without configuring its API proxy.

Going further

  • Add a shared server cache and combine simultaneous requests before opening the page to more visitors
  • Pause polling while the browser tab is hidden to save calls
  • Add a daily chart with /v1/bars using the API reference
  • Use /v1/convert for prices per gram or kilogram
  • Compare the server setup with the Node tutorial

Questions

Where should I keep the API key?

Keep the goldprice.dev API key in GOLDPRICE_API_KEY on your Express server. React code runs in the browser, where visitors can inspect bundled JavaScript and requests. The browser should call your own route, which adds the key only when contacting goldprice.dev.

Will automatic refresh fit the free tier quota?

The Free plan includes 5,000 calls a month and 10 requests a minute. Continuous polling every minute exceeds the monthly allowance, even for a single tab. This sample makes a server request for each refresh. For a public site, share a server cache and choose a refresh schedule that fits your quota.

Are Free plan gold prices live?

Free and keyless gold prices are delayed by fifteen minutes. Paid plans are live. Refreshing the page more often does not remove that delay. Use computed_at to show the calculation time and check is_stale for a stale response. A false stale flag does not mean Free prices are live.

What happens when the component unmounts?

The cleanup function clears the refresh interval and aborts the current browser request. A stopped flag prevents late results from updating state. React development Strict Mode can run an extra setup and cleanup cycle, so an additional initial request can appear during development.

Why does the price stay a string?

The goldprice.dev API returns the price as a decimal string. This component displays that string directly, preserving the returned digits. If you add calculations, use decimal arithmetic and round only for the final display. Converting through JavaScript Number can introduce rounding errors.

Can I ask Claude to write this React component?

Yes. Ask Claude to create a React function component that calls your Express route, shows loading and error states, displays computed_at and is_stale, and refreshes with cleanup. Tell it to keep GOLDPRICE_API_KEY on the server. Review and run the generated code before publishing.

Can I show the gold price in another currency?

Yes. Change the symbol in the server URL to XAU-EUR-SPOT or XAU-GBP-SPOT, then update the currency label in React to match. The spot response keeps the same shape, with its currency identified by quote_currency. Keep the price as a decimal string.

Keep reading

Start with a free API key

5,000 requests a month on the free plan. No credit card.

Get a free key