Skip to main content

Create a money exchange app

One of the primary purposes of Gosoline is to help you build an HTTP server. An HTTP server, in the context of Gosoline, is a module that:

  • Runs indefinitely
  • Listens to a port for requests
  • Provides responses to those requests

Our httpserver package provides a convenient way to create HTTP servers.

In this tutorial, you'll create a money exchange web service. This service will have two endpoints:

GET /euro/{AMOUNT IN SOURCE CURRENCY}/{SOURCE CURRENCY}
GET /euro-at-date/{AMOUNT IN SOURCE CURRENCY}/{SOURCE CURRENCY}/{EXCHANGE RATE DATE}

Both of these endpoints:

  • Accept an amount in a source currency.
  • Convert that amount to euros based on an exchange rate.

The euro-at-date endpoint allows you to specify a historical date for the exchange rate.

Before you begin

Before you begin, make sure you have Golang installed on your machine.

Set up your file structure

First, you need to set up the following file structure:

money-exchange/
├── handler.go
├── definer.go
├── config.dist.yml
└── main.go

For example, in Unix, run:

mkdir money-exchange; cd money-exchange
touch handler.go
touch definer.go
touch config.dist.yml
touch main.go

Those are all the files you need to build your web service with gosoline! Next, you'll implement each of these files, starting with handler.go.

Implement handler.go

In handler.go, add the following code:

handler.go
package main

import (
"context"
"fmt"
"net/http"
"strconv"
"time"

"github.com/justtrackio/gosoline/pkg/cfg"
"github.com/justtrackio/gosoline/pkg/currency"
"github.com/justtrackio/gosoline/pkg/httpserver"
"github.com/justtrackio/gosoline/pkg/log"
)


type euroHandler struct {
logger log.Logger
currencyService currency.Service
}


func NewEuroHandler(ctx context.Context, config cfg.Config, logger log.Logger) (*euroHandler, error) {
// Instantiate a new currencyService
currencyService, err := currency.New(ctx, config, logger)
if err != nil {
return nil, fmt.Errorf("can not create currencyService: %w", err)
}

// Return a euroHandler
return &euroHandler{
logger: logger,
currencyService: currencyService,
}, nil
}


func (h *euroHandler) Handle(requestContext context.Context, request *httpserver.Request) (response *httpserver.Response, err error) {
// Get a currency and amountString from the request parameters.
currency := request.Params.ByName("currency")
amountString := request.Params.ByName("amount")

// Parse a float value from amountString.
amount, err := strconv.ParseFloat(amountString, 64)
// Send a 400 Bad Request response if amountString can't be parsed into a valid float.
if err != nil {
h.logger.Error("cannot parse amount %s: %w", amountString, err)

return httpserver.NewStatusResponse(http.StatusBadRequest), nil
}

// Convert the amount from the source currency to euros.
result, err := h.currencyService.ToEur(requestContext, amount, currency)
// Send a 500 Internal Server Error if the server can't convert the amount.
if err != nil {
h.logger.Error("cannot convert amount %f with currency %s: %w", amount, currency, err)

return httpserver.NewStatusResponse(http.StatusInternalServerError), nil
}

// Send a 200 OK Json response back to the client with the results.
return httpserver.NewJsonResponse(result), nil
}


type euroAtDateHandler struct {
logger log.Logger
currencyService currency.Service
}

func NewEuroAtDateHandler(ctx context.Context, config cfg.Config, logger log.Logger) (*euroAtDateHandler, error) {
currencyService, err := currency.New(ctx, config, logger)
if err != nil {
return nil, fmt.Errorf("can not create currencyService: %w", err)
}

return &euroAtDateHandler{
logger: logger,
currencyService: currencyService,
}, nil
}


func (h *euroAtDateHandler) Handle(requestContext context.Context, request *httpserver.Request) (response *httpserver.Response, err error) {
// Get the request parameters and parse their string values.
currency := request.Params.ByName("currency")
dateString := request.Params.ByName("date")
date, err := time.Parse(time.RFC3339, dateString)
// Send a 500 Internal Server Error if the service can't parse the params or convert the currency.
if err != nil {
h.logger.Error("cannot parse date %s: %w", dateString, err)

return httpserver.NewStatusResponse(http.StatusInternalServerError), nil
}

amountString := request.Params.ByName("amount")
amount, err := strconv.ParseFloat(amountString, 64)
if err != nil {
h.logger.Error("cannot parse amount %s: %w", amountString, err)

return httpserver.NewStatusResponse(http.StatusInternalServerError), nil
}

result, err := h.currencyService.ToEurAtDate(requestContext, amount, currency, date)
if err != nil {
h.logger.Error("cannot convert amount %f with currency %s at date %v: %w", amount, currency, date, err)

return httpserver.NewStatusResponse(http.StatusInternalServerError), nil
}

// Send a 200 OK Json response back to the client with the results.
return httpserver.NewJsonResponse(result), nil
}

Now, you'll walkthrough this file in detail to learn how it works.

Import your dependencies

At the top of handler.go, you declared the package and imported some dependencies:

handler.go
package main

import (
"context"
"fmt"
"net/http"
"strconv"
"time"

"github.com/justtrackio/gosoline/pkg/cfg"
"github.com/justtrackio/gosoline/pkg/currency"
"github.com/justtrackio/gosoline/pkg/httpserver"
"github.com/justtrackio/gosoline/pkg/log"
)

Here, you declared the package as main. Then, you imported several standard modules along with four gosoline dependencies:

Define a euroHandler structure

Next, you created a euroHandler struct:

handler.go
type euroHandler struct {
logger log.Logger
currencyService currency.Service
}

You'll use this in a few places to carry data about your logger and currency service. You'll also implement its Handle() method to handle HTTP requests.

Define a handler

Then, you implemented a function for creating new euroHandler structs:

handler.go
func NewEuroHandler(ctx context.Context, config cfg.Config, logger log.Logger) (*euroHandler, error) {
// Instantiate a new currencyService
currencyService, err := currency.New(ctx, config, logger)
if err != nil {
return nil, fmt.Errorf("can not create currencyService: %w", err)
}

// Return a euroHandler
return &euroHandler{
logger: logger,
currencyService: currencyService,
}, nil
}

You'll use this later to create a new euroHandler.

Implement a request handler

Then, you implemented euroHandler.Handle() for handling HTTP requests:

handler.go
func (h *euroHandler) Handle(requestContext context.Context, request *httpserver.Request) (response *httpserver.Response, err error) {
// Get a currency and amountString from the request parameters.
currency := request.Params.ByName("currency")
amountString := request.Params.ByName("amount")

// Parse a float value from amountString.
amount, err := strconv.ParseFloat(amountString, 64)
// Send a 400 Bad Request response if amountString can't be parsed into a valid float.
if err != nil {
h.logger.Error("cannot parse amount %s: %w", amountString, err)

return httpserver.NewStatusResponse(http.StatusBadRequest), nil
}

// Convert the amount from the source currency to euros.
result, err := h.currencyService.ToEur(requestContext, amount, currency)
// Send a 500 Internal Server Error if the server can't convert the amount.
if err != nil {
h.logger.Error("cannot convert amount %f with currency %s: %w", amount, currency, err)

return httpserver.NewStatusResponse(http.StatusInternalServerError), nil
}

// Send a 200 OK Json response back to the client with the results.
return httpserver.NewJsonResponse(result), nil
}

Define a euroAtDateHandler

Like you did with euroHandler, you defined a euroAtDateHandler struct and a corresponding constructor:

handler.go
type euroAtDateHandler struct {
logger log.Logger
currencyService currency.Service
}

func NewEuroAtDateHandler(ctx context.Context, config cfg.Config, logger log.Logger) (*euroAtDateHandler, error) {
currencyService, err := currency.New(ctx, config, logger)
if err != nil {
return nil, fmt.Errorf("can not create currencyService: %w", err)
}

return &euroAtDateHandler{
logger: logger,
currencyService: currencyService,
}, nil
}

The logic here is very similar to the logic for euroHandler.

Implement a second request handler

Finally, you implemented euroAtDateHandler.Handle() for handling HTTP requests:

handler.go
func (h *euroAtDateHandler) Handle(requestContext context.Context, request *httpserver.Request) (response *httpserver.Response, err error) {
// Get the request parameters and parse their string values.
currency := request.Params.ByName("currency")
dateString := request.Params.ByName("date")
date, err := time.Parse(time.RFC3339, dateString)
// Send a 500 Internal Server Error if the service can't parse the params or convert the currency.
if err != nil {
h.logger.Error("cannot parse date %s: %w", dateString, err)

return httpserver.NewStatusResponse(http.StatusInternalServerError), nil
}

amountString := request.Params.ByName("amount")
amount, err := strconv.ParseFloat(amountString, 64)
if err != nil {
h.logger.Error("cannot parse amount %s: %w", amountString, err)

return httpserver.NewStatusResponse(http.StatusInternalServerError), nil
}

result, err := h.currencyService.ToEurAtDate(requestContext, amount, currency, date)
if err != nil {
h.logger.Error("cannot convert amount %f with currency %s at date %v: %w", amount, currency, date, err)

return httpserver.NewStatusResponse(http.StatusInternalServerError), nil
}

// Send a 200 OK Json response back to the client with the results.
return httpserver.NewJsonResponse(result), nil
}

The logic here is very similar to the logic for euroHandler.Handle().

Now, you've fully implemented your request handlers. Next, you'll create a Definer object.

Implement definer.go

In definer.go, add the following code:

definer.go
package main

import (
"context"
"fmt"

"github.com/justtrackio/gosoline/pkg/cfg"
"github.com/justtrackio/gosoline/pkg/httpserver"
"github.com/justtrackio/gosoline/pkg/log"
)


func ApiDefiner(ctx context.Context, config cfg.Config, logger log.Logger) (*httpserver.Definitions, error) {
// Create an empty Definitions object, called definitions.
definitions := &httpserver.Definitions{}

// Create a new euroHandler.
euroHandler, err := NewEuroHandler(ctx, config, logger)
if err != nil {
return nil, fmt.Errorf("can not create euroHandler: %w", err)
}

// Create a new euroAtDateHandler.
euroAtDateHandler, err := NewEuroAtDateHandler(ctx, config, logger)
if err != nil {
return nil, fmt.Errorf("can not create euroAtDateHandler: %w", err)
}

// Add two routes to definitions. Each route handles GET requests. Notice that each route uses one of the handlers you wrote in handlers.go.
definitions.GET("/euro/:amount/:currency", httpserver.CreateHandler(euroHandler))
definitions.GET("/euro-at-date/:amount/:currency/:date", httpserver.CreateHandler(euroAtDateHandler))

// Return definitions.
return definitions, nil
}

Now, you'll walkthrough this file in detail to learn how it works.

Import dependencies

At the top of definer.go, you declared the package and imported some dependencies:

definer.go
package main

import (
"context"
"fmt"

"github.com/justtrackio/gosoline/pkg/cfg"
"github.com/justtrackio/gosoline/pkg/httpserver"
"github.com/justtrackio/gosoline/pkg/log"
)

Here, you declared the package as main. Then, you imported the standard context and fmt modules along with three gosoline dependencies:

Implement a definer

Then, you implemented ApiDefiner:

definer.go
func ApiDefiner(ctx context.Context, config cfg.Config, logger log.Logger) (*httpserver.Definitions, error) {
// Create an empty Definitions object, called definitions.
definitions := &httpserver.Definitions{}

// Create a new euroHandler.
euroHandler, err := NewEuroHandler(ctx, config, logger)
if err != nil {
return nil, fmt.Errorf("can not create euroHandler: %w", err)
}

// Create a new euroAtDateHandler.
euroAtDateHandler, err := NewEuroAtDateHandler(ctx, config, logger)
if err != nil {
return nil, fmt.Errorf("can not create euroAtDateHandler: %w", err)
}

// Add two routes to definitions. Each route handles GET requests. Notice that each route uses one of the handlers you wrote in handlers.go.
definitions.GET("/euro/:amount/:currency", httpserver.CreateHandler(euroHandler))
definitions.GET("/euro-at-date/:amount/:currency/:date", httpserver.CreateHandler(euroAtDateHandler))

// Return definitions.
return definitions, nil
}

Here, the :amount, :currency, etc. constructs are path parameters. This means the handler will be able to access and use them. Indeed, you already implemented this behavior in your handler.go file. euroHandler.Handle() gets these values like this:

currency := request.Params.ByName("currency")
amountString := request.Params.ByName("amount")

Now that you've created handlers and a definer, create a configuration file.

Configure your server

In config.dist.yml, configure your server:

config.dist.yml
env: dev

app_project: gosoline
app_family: example
app_group: money
app_name: exchange

httpserver:
default:
port: 8080

kvstore:
currency:
type: chain
in_memory:
max_size: 500000
application: money-exchange
elements: [inMemory]
ttl: 30m

Here, you set some minimal configurations for your web server. There are a few interesting configurations to note:

  • httpserver.default.port exposes port 8080. (In your applications, you can configure more aspects of the server in a similar manner.)
  • The currency key value store (kvstore) is defined as inMemory and serves as a local database. The currency module that you used in handler.go uses this to store the exchange rates for various currencies:
    • First, it makes an initial call to an external endpoint in order to get exchange rates and stores them in a kvstore
    • Later, it occasionally makes more calls to obtain exchange rates, in order to keep the kvstore updated

At this point, you've implemented your handlers, a definer, and your app configuration. Next, you'll implement main.go that puts it all together.

Implement main.go

In main.go, add the following code:

main.go
package main

import (
"github.com/justtrackio/gosoline/pkg/application"
"github.com/justtrackio/gosoline/pkg/currency"
"github.com/justtrackio/gosoline/pkg/httpserver"
)


func main() {
application.Run(
application.WithConfigFile("config.dist.yml", "yml"),
application.WithLoggerHandlersFromConfig,

application.WithModuleFactory("api", httpserver.New("default", ApiDefiner)),
application.WithModuleFactory("currency", currency.NewCurrencyModule()),
)
}

Now, you'll walkthrough this file in detail to learn how it works.

Import your dependencies

At the top of main.go, you declared the package and imported some dependencies:

main.go
package main

import (
"github.com/justtrackio/gosoline/pkg/application"
"github.com/justtrackio/gosoline/pkg/currency"
"github.com/justtrackio/gosoline/pkg/httpserver"
)

Here, you declared the package as main. Then, you imported three gosoline dependencies:

Implement main()

Then, you implemented the main entry point for your web service:

main.go
func main() {
application.Run(
application.WithConfigFile("config.dist.yml", "yml"),
application.WithLoggerHandlersFromConfig,

application.WithModuleFactory("api", httpserver.New("default", ApiDefiner)),
application.WithModuleFactory("currency", currency.NewCurrencyModule()),
)
}

Here, you run a kernel that uses config.dist.yml for its configuration and uses the api and currency modules.

note

Notice the api module is using your ApiDefiner that, in turn, uses your handlers.

Now that you've wired the application up, the final step is to test it to confirm that it works as expected.

Run your application

Initialize your go module, install the dependencies, and run your web service:

go mod init money_exchange/m
go mod tidy
go run .

In a separate terminal, make requests to your service:

curl localhost:8080/euro/10/GBP
curl localhost:8080/euro-at-date/10/USD/2021-01-03T00:00:00Z

Conclusion

Having seen a sample HTTP server, you can now look into more detailed functionality, such as writing integration tests. Check out these resources to learn more about creating web services with gosoline: