The standard library is enough for a reliable Go integration. Keep the API key in the process environment, reject non-2xx responses before decoding JSON, and retain source, market_session, and data_updated_at beside every rate.
Create a typed client with net/http
Save this complete program as main.go. It requests three symbols in one call and preserves numeric tokens as json.Number; use an exact-decimal library before doing money arithmetic. The anonymous request is useful for evaluation; set EXCHANGERATE_API_KEY when the application has a key. context.WithTimeout prevents a stalled network connection from holding a worker forever.
package main
import (
"context"
"encoding/json"
"fmt"
"net/http"
"log"
"os"
"regexp"
"strings"
"time"
)
type Latest struct {
Base string `json:"base"`
Source string `json:"source"`
MarketSession string `json:"market_session"`
UpdatedAt time.Time `json:"data_updated_at"`
Sources map[string]string `json:"sources"`
EffectiveAt map[string]string `json:"effective_at"`
Rates map[string]json.Number `json:"rates"`
}
func LatestRates(ctx context.Context, base string) (Latest, error) {
base = strings.ToUpper(base)
if !regexp.MustCompile("^[A-Z]{3}$").MatchString(base) {
return Latest{}, fmt.Errorf("invalid base currency: %q", base)
}
url := "https://api.exchangerate.dev/v1/latest/" + base + "?symbols=EUR,GBP,JPY"
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil { return Latest{}, err }
if key := os.Getenv("EXCHANGERATE_API_KEY"); key != "" {
req.Header.Set("Authorization", "Bearer "+key)
}
client := &http.Client{Timeout: 5 * time.Second, CheckRedirect: func(_ *http.Request, _ []*http.Request) error {
return http.ErrUseLastResponse
}}
res, err := client.Do(req)
if err != nil { return Latest{}, err }
defer res.Body.Close()
if res.StatusCode < 200 || res.StatusCode >= 300 {
return Latest{}, fmt.Errorf("exchange rate API: HTTP %s", res.Status)
}
var out Latest
if err := json.NewDecoder(res.Body).Decode(&out); err != nil { return Latest{}, err }
return out, nil
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
latest, err := LatestRates(ctx, "USD")
if err != nil { log.Fatal(err) }
fmt.Println(latest.Rates["EUR"], latest.Sources["EUR"], latest.EffectiveAt["EUR"])
}Call it with a timeout
go run main.goHandle errors and freshness explicitly
Do not decode an error body as if it were a successful rate response. A 401 means the key or authorization header needs correction. A 429 means the caller should slow down and respect its plan. Retry only bounded, transient failures such as a network timeout or selected 5xx responses, with backoff.
source, market_session, and data_updated_at with the number. All 144 currencies update about every 60 seconds on trading days; ECB fixes provide daily fallback.- Reuse an http.Client with an explicit transport timeout in production.
- Cache only successful responses.
- Use decimal arithmetic for money calculations at the application boundary.
- Never expose EXCHANGERATE_API_KEY in browser or mobile builds.
