Hey Guys!
Yet another small mini-project for making a currency converter in Go!
It's a simple but fun project that should take someone about an hour or 2 depending on their experience. It takes a currency type from one end, the currency which we intend to convert to and the amount to be converted.
I'm making use of a third-party service (https://openexchangerates.org) to retrieve the latest currency data.
My Main base currencies are:
1) USD 2) EUR 3) GBP 4) JPY and I also have support for "other" currencies through input in the TUI. Both for base currencies and currencies to be converted to.
~ Source Code: Found here
Let's Begin
What is your base currency? List $ USD United States Dollar £ GBP British Pound € EUR Euro ¥ JPY Japanese Yen ••• The main functionalities of the application are:
- Get Conversion details entered by the user
- Use those details and send an API request to Openxchangerates (Third-Party service with the latest currency conversion rates)
- Convert currencies & amount
- Output currencies & amount data to the user
The packages used:
-
net/http- for http requests to the currency exchange api -
github.com/charmbracelet/huh- for the TUI interface form -
github.com/charmbracelet/bubbles/list- feature-rich for browsing a general-purpose list of items -
encoding/json- in order to marshal the data for the API -
github.com/charmbracelet/lipgloss- Style definitions for terminal layouts
How does it work?
So let's discuss the first bit of functionality, which is getting conversion details from the user.
A view method for getting this user-provided data had to be made and it asks the user questions on what currency to convert, which currency to be converted to, and more.
func (m model) View() string { if m.err != nil { return fmt.Sprintf("Error: %v\n\nPress any key to continue.\n", m.err) } if m.finished { // Return an empty string when finished to avoid redundant output. return "" } switch m.stage { case 0: if m.isCustomInput { return questionStyle.Render("Enter your custom base currency code (e.g., USD):\n\n") + m.textInput.View() } return questionStyle.Render("What is your base currency?\n\n") + m.list.View() case 1: if m.isCustomInput { return questionStyle.Render("Enter your custom target currency code (e.g., EUR):\n\n") + m.textInput.View() } return questionStyle.Render("What do you want to convert to?\n\n") + m.list.View() case 2: return questionStyle.Render("How much to convert?\n\n") + m.textInput.View() default: return "" } } What do you want to convert to? List $ USD United States Dollar £ GBP British Pound ••• How much to convert? > 200 Now let's discuss the second point, using the currency conversion details and sending an API request to Openxchangerates.
Here I'm getting/fetching for the latest currency rates from Openxchangerates.org via an API key provided by the third-party currency exchange platform. In my case, I made use of .env's for secret management but there are a multitude of other ways to better handle this, especially if it were a production app.
~ Openxchangerates.org docs used for this: located here
//api.go package api import ( "encoding/json" "fmt" "net/http" ) type CurrencyData struct { Base string `json:"base"` Rates map[string]float64 `json:"rates"` } func FetchRates(apiKey string) (CurrencyData, error) { url := fmt.Sprintf("https://openexchangerates.org/api/latest.json?app_id=%s&prettyprint=false", apiKey) resp, err := http.Get(url) if err != nil { return CurrencyData{}, err } defer resp.Body.Close() if resp.StatusCode != 200 { return CurrencyData{}, fmt.Errorf("API request failed with status: %s", resp.Status) } var data CurrencyData err = json.NewDecoder(resp.Body).Decode(&data) if err != nil { return CurrencyData{}, err } return data, nil } Then we'll proceed with converting the currencies and amounts:
//conversion.go package conversion func Convert(amount float64, rateFrom, rateTo float64) float64 { return amount * (rateTo / rateFrom) } which is simply taking in a base amount (amount), a rate from the base currency and the rate to be converted to and returns a final converted amount.
Lastly, providing the converted currencies and amount-related data back to the user.
Controlling the logic behind these selections is an update function that serves as the main state transition handler for the application's model.
func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd switch msg := msg.(type) { case tea.KeyMsg: key := msg.String() switch key { case "enter": if m.isCustomInput { // Handle custom currency input input := strings.ToUpper(strings.TrimSpace(m.textInput.Value())) if !isAlphabetic(input) || len(input) != 3 { m.err = fmt.Errorf("invalid currency code: must be 3 letters") return m, nil } if m.stage == 0 { m.currencyFrom = input m.isCustomInput = false m.textInput.Reset() m.stage++ m.list.ResetSelected() return m, nil } else if m.stage == 1 { m.currencyTo = input m.isCustomInput = false m.textInput.Reset() m.stage++ m.textInput.Placeholder = "Enter amount (e.g., 100)" m.textInput.Focus() return m, nil } } else if m.stage == 2 { // Handle amount input amountStr := strings.TrimSpace(m.textInput.Value()) amount, err := strconv.ParseFloat(amountStr, 64) if err != nil { m.err = fmt.Errorf("invalid amount: %v", err) return m, nil } m.amount = amount m.finished = true return m, tea.Quit } else { // Handle list selection selectedItem := m.list.SelectedItem().(Item) if selectedItem.Code == "OTHER" { m.isCustomInput = true m.textInput.Placeholder = "Enter currency code (e.g., USD)" m.textInput.Focus() return m, textinput.Blink } if m.stage == 0 { m.currencyFrom = selectedItem.Code m.stage++ m.list.ResetSelected() return m, nil } else if m.stage == 1 { m.currencyTo = selectedItem.Code m.stage++ m.textInput.Placeholder = "Enter amount (e.g., 100)" m.textInput.Focus() return m, nil } } case "ctrl+c", "esc": return m, tea.Quit } } // Handle text input for custom currency or amount input if m.isCustomInput || m.stage == 2 { m.textInput, cmd = m.textInput.Update(msg) return m, cmd } // Handle list updates for currency selection if m.stage == 0 || m.stage == 1 { m.list, cmd = m.list.Update(msg) return m, cmd } return m, nil } // go run main.go 200.00 EUR = 166.03 GBP Conclusion
That pretty much wraps up this relatively quick currency converter. I hope you've enjoyed the quick read and feel free to give a shot also, it's not that bad! 😁.
Feel free to also experiment with other third-party currency exchange providers out there, there are many. Hopefully, they got a decent API too!
See you guys on the next one! 👋🏼
Top comments (0)