# Add currency exchange rates and conversion to Go

Use net/http and encoding/json with struct tags. Cache latest responses until nextUpdateAt.

## Get a free API key

Sign up at https://currencyexchangerate-api.com/auth/login, then create a key under API keys in the console. The free plan includes 3,000 requests per month with no credit card. Store the secret as CXR_API_KEY in server-side environment configuration; never expose it in browser code.

File: `.env`

```shell
CXR_API_KEY=cxr_your_secret_key
```

## Create a reusable client and cache rates

Use the standard HTTP client. Cache every /latest response until nextUpdateAt: rates change at most once by then, and reusing the response saves quota. The helper also exposes /convert.

File: `rates/rates.go`

```go
package rates

import (
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
	"os"
	"sync"
	"time"
)

const apiBase = "https://api.currencyexchangerate-api.com/api/v1"

type Latest struct {
	Base         string             `json:"base"`
	UpdatedAt    time.Time          `json:"updatedAt"`
	NextUpdateAt time.Time          `json:"nextUpdateAt"`
	Rates        map[string]float64 `json:"rates"`
}

type Conversion struct {
	From      string    `json:"from"`
	To        string    `json:"to"`
	Amount    float64   `json:"amount"`
	Rate      float64   `json:"rate"`
	Result    float64   `json:"result"`
	UpdatedAt time.Time `json:"updatedAt"`
}

type APIError struct {
	Status     int
	RetryAfter string
}

func (e *APIError) Error() string {
	return fmt.Sprintf("rates API status %d (Retry-After %q)", e.Status, e.RetryAfter)
}

var (
	mu     sync.Mutex
	cached = map[string]*Latest{}
)

func request(path string, out any) error {
	req, err := http.NewRequest("GET", apiBase+path, nil)
	if err != nil {
		return err
	}
	req.Header.Set("Authorization", "Bearer "+os.Getenv("CXR_API_KEY"))

	res, err := http.DefaultClient.Do(req)
	if err != nil {
		return err
	}
	defer res.Body.Close()

	if res.StatusCode >= 400 {
		return &APIError{res.StatusCode, res.Header.Get("Retry-After")}
	}
	return json.NewDecoder(res.Body).Decode(out)
}

func LatestRates(base, symbols string) (*Latest, error) {
	id := base + ":" + symbols
	mu.Lock()
	hit := cached[id]
	mu.Unlock()
	if hit != nil && time.Now().Before(hit.NextUpdateAt) {
		return hit, nil
	}

	q := url.Values{"base": {base}, "symbols": {symbols}}
	data := &Latest{}
	if err := request("/latest?"+q.Encode(), data); err != nil {
		return nil, err
	}

	mu.Lock()
	cached[id] = data
	mu.Unlock()
	return data, nil
}

func Convert(from, to string, amount float64) (Conversion, error) {
	q := url.Values{
		"from":   {from},
		"to":     {to},
		"amount": {fmt.Sprint(amount)},
	}
	var data Conversion
	err := request("/convert?"+q.Encode(), &data)
	return data, err
}
```

## Use it in Go

Call the helper from a server route, controller, view, command, or script. Format converted values for the user’s locale.

File: `main.go`

```go
package main

import (
	"fmt"
	"log"

	"example.com/app/rates"
)

func main() {
	result, err := rates.Convert("EUR", "GBP", 100)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("%s %.2f\n", result.To, result.Result)
}
```

## Handle API errors

A 401 means the key is invalid. A 429 can mean a rate limit or quota limit; read Retry-After in seconds before retrying. Other errors use application/problem+json.

File: `errors.go`

```go
package main

import (
	"errors"
	"log"

	"example.com/app/rates"
)

func main() {
	_, err := rates.Convert("EUR", "GBP", 100)

	var apiErr *rates.APIError
	if errors.As(err, &apiErr) {
		switch apiErr.Status {
		case 401:
			log.Fatal("check CXR_API_KEY")
		case 429:
			log.Fatalf("retry after %s seconds", apiErr.RetryAfter)
		}
	}
	if err != nil {
		log.Fatal(err)
	}
}
```

## Format the amount for display

Use locale-aware currency formatting instead of adding a symbol manually.

File: `format.go`

```go
package main

import (
	"golang.org/x/text/language"
	"golang.org/x/text/message"
)

func format(code string, value float64) string {
	p := message.NewPrinter(language.BritishEnglish)
	return p.Sprintf("%s %.2f", code, value)
}
```

## Related links

- [API documentation](https://currencyexchangerate-api.com/docs.md)
- [All integration guides](https://currencyexchangerate-api.com/guides.md)
- [Get a free API key](https://currencyexchangerate-api.com/auth/login)
