Implement transfer money API with a custom params validator in Go
Tags: go, tutorial, webdev, beginners
Published: 2020-12-07 21:46:24

In the previous lectures, we have implemented and tested the HTTP APIs to mange bank accounts for our simple bank project.

Today, we will do some more practice by implementing the most important API of our application: `transfer money API`.

And while doing so, I will show you how to write a custom validator to validate the input parameters of this API.

{% youtube 5q_wsashJZA %}

Here's:

- Link to the [full series playlist on Youtube](https://bit.ly/backendmaster)
- And its [Github repository](https://github.com/techschool/simplebank)

## Implement the transfer money API handler

First I will create a new `transfer.go` file inside the `api` package. The implementation of the transfer money API will be very similar to that of the create account API.

The struct to store input parameters of this API should be `transferRequest`. It will have several fields:

```go
type transferRequest struct {
    FromAccountID int64  `json:"from_account_id" binding:"required,min=1"`
    ToAccountID   int64  `json:"to_account_id" binding:"required,min=1"`
    Amount        int64  `json:"amount" binding:"required,gt=0"`
    Currency      string `json:"currency" binding:"required,oneof=USD EUR CAD"`
}
```

- The `FromAccountID` of type `int64` is the ID of the account where money is going out. This field is required, and its minimum value should be 1.
- Similarly, the `ToAccountID`, also of type `int64`, is the ID of the account where the money is going in.
- Next, we have the `Amount` field to store the amount of money to transfer between 2 accounts. For simplicity, here I just use integer type. But in reality, it could be a real number, depending on the currency. So you should keep that in mind and choose the appropriate type for your data. For the binding condition, this field is also required and it should be greater than 0.
- The last field is the `Currency` of the money we want to transfer. For now, we only allow it to be either `USD`, `EUR` or `CAD`. And note that this currency should match the currency of both 2 accounts. We will verify that in the API handler function.

Alright, now the handler function’s name should be `createTransfer`. And the request variable’s type should be `transferRequest`.

We bind the input parameters to the request object, and return `http.StatusBadRequest` to the client if the any of the parameters is invalid.

```go
func (server *Server) createTransfer(ctx *gin.Context) {
    var req transferRequest
    if err := ctx.ShouldBindJSON(&req); err != nil {
        ctx.JSON(http.StatusBadRequest, errorResponse(err))
        return
    }

    arg := db.TransferTxParams{
        FromAccountID: req.FromAccountID,
        ToAccountID:   req.ToAccountID,
        Amount:        req.Amount,
    }

    result, err := server.store.TransferTx(ctx, arg)
    if err != nil {
        ctx.JSON(http.StatusInternalServerError, errorResponse(err))
        return
    }

    ctx.JSON(http.StatusOK, result)
}
```

Next we have to create a `db.TransferTxParam` object, where `FromAccountID` is `request.fromAccountID`, `ToAccountID` is `request.toAccountID`, and `Amount` is `request.Amount`.

With this argument, we call `server.store.TransferTx()` to perform the money transfer transaction. This function will return a `TransferTxResult` object or an error. At the end, we just need to return that result to the client if no errors occur.

Alright, this create transfer handler is almost finished except that we haven’t taken into account the last input parameter: `request.Currency`.

What we need to do is to compare this currency with the currency of the from account and to account to make sure that they’re all the same. So I’m gonna define a new function called `validAccount()` for the `Server` struct.

This function will check if an account with a specific ID really exists, and its currency matches the input currency. Therefore, it will have 3 input arguments: A `gin.Context`, an `accountID`, and a `currency` string. And it will return a `bool` value.

```go
func (server *Server) validAccount(ctx *gin.Context, accountID int64, currency string) bool {
    account, err := server.store.GetAccount(ctx, accountID)
    if err != nil {
        if err == sql.ErrNoRows {
            ctx.JSON(http.StatusNotFound, errorResponse(err))
            return false
        }

        ctx.JSON(http.StatusInternalServerError, errorResponse(err))
        return false
    }

    if account.Currency != currency {
        err := fmt.Errorf("account [%d] currency mismatch: %s vs %s", account.ID, account.Currency, currency)
        ctx.JSON(http.StatusBadRequest, errorResponse(err))
        return false
    }

    return true
}
```

First, we call `server.store.GetAccount()` to query the account from the database. This function will return an `account` object or an error. If error is not `nil`, then there are 2 possible scenarios:

- The first scenario is when the account doesn’t exist, then we send `http.StatusNotFound` to the client and return `false`.
- The second scenario is when some unexpected errors occur, so we just send `http.StatusInternalServerError` and return `false`.

Otherwise, if there’s no error, we will check if the account’s currency matches the input currency or not. If it doesn’t match, we declare a new error: account currency mismatch. Then we call `ctx.JSON()` with the `http.StatusBadRequest` to send this error response to the client, and return `false`.

Finally, if everything is good, and the account is valid, we return `true` at the end of this function.

Now let’s go back to the createTransfer handler.

```go
func (server *Server) createTransfer(ctx *gin.Context) {
    var req transferRequest
    if err := ctx.ShouldBindJSON(&req); err != nil {
        ctx.JSON(http.StatusBadRequest, errorResponse(err))
        return
    }

    if !server.validAccount(ctx, req.FromAccountID, req.Currency) {
        return
    }

    if !server.validAccount(ctx, req.ToAccountID, req.Currency) {
        return
    }

    arg := db.TransferTxParams{
        FromAccountID: req.FromAccountID,
        ToAccountID:   req.ToAccountID,
        Amount:        req.Amount,
    }

    result, err := server.store.TransferTx(ctx, arg)
    if err != nil {
        ctx.JSON(http.StatusInternalServerError, errorResponse(err))
        return
    }

    ctx.JSON(http.StatusOK, result)
}
```

We call `server.validAccount()` to check the validity of the `request.fromAccountID` and `currency`. If it’s not valid, then we just return immediately. We do the same thing for the `request.toAccountID`.

And that’s it! The `createTransfer` handler is completed. Next we have to register a new API in the server to route requests to this handler.

## Register the transfer money API route

Let’s open the `api/server.go` file.

I’m gonna duplicate the `router.POST` account API. Then change the path to `/transfers`, and the handler’s name to `server.createTransfer`.

```go
func NewServer(store db.Store) *Server {
    server := &Server{store: store}
    router := gin.Default()

    router.POST("/accounts", server.createAccount)
    router.GET("/accounts/:id", server.getAccount)
    router.GET("/accounts", server.listAccounts)
    router.POST("/transfers", server.createTransfer)

    server.router = router
    return server
}
```

That’s all. Let’s open the terminal and run:

```bash
❯ make server
```

## Test the transfer money API

Then I’m gonna use [Postman](https://www.postman.com/) to test the new transfer money API.

Let's create a new request with method `POST`, and the URL is `http://localhost:8080/transfers`.

For the request body, let’s select raw, and choose JSON format. I’m gonna use this sample JSON body:

```json
{
    "from_account_id": 1,
    "to_account_id": 2,
    "amount": 10,
    "currency": "USD"
}
```

The from account id is `1`, the to account id is `2`, the amount is `10`, and the currency is `USD`.

Let’s open [TablePlus](https://tableplus.com/) to see the current data of these 2 accounts.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/yyi3npymo4s0yejflbdo.jpg)

Here we can see that their currencies are different. The currency of account `2` is `USD`, but that of account `1` is `CAD`.

So if we send this API request, we will get a currency mismatch error for the account `1`.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/gvha0lub8egg77s0kyx6.jpg)

To fix this, I will update the currency of account `1` to `USD` in TablePlus. Save it, and go back to Postman to send the request again.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/sx6cyjud9r10cu95lf04.jpg)

This time the request is successful.

```json
{
    "transfer": {
        "id": 110,
        "from_account_id": 1,
        "to_account_id": 2,
        "amount": 10,
        "created_at": "2020-12-07T19:57:09.61222Z"
    },
    "from_account": {
        "id": 1,
        "owner": "ojkelz",
        "balance": 734,
        "currency": "USD",
        "created_at": "2020-11-28T15:22:13.419691Z"
    },
    "to_account": {
        "id": 2,
        "owner": "ygmlfb",
        "balance": 824,
        "currency": "USD",
        "created_at": "2020-11-28T15:22:13.435304Z"
    },
    "from_entry": {
        "id": 171,
        "account_id": 1,
        "amount": -10,
        "created_at": "2020-12-07T19:57:09.61222Z"
    },
    "to_entry": {
        "id": 172,
        "account_id": 2,
        "amount": 10,
        "created_at": "2020-12-07T19:57:09.61222Z"
    }
}
```

And in the response, we have a transfer record with id `110` from account `1` to account `2` with amount of `10`. The new balance of account `1` is `734 USD`. While that of account `2` is `824 USD`.


Let’s check the database records in `TablePlus`. Before the transaction, the original values of account `1` and account `2` were `744` and `814 USD`.

When I press command R to refresh the data, we can see that the balance of account `1` has decreased by `10`, and the balance of account `2` has increased by `10`.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/ejt099ctejzmfd97v0jh.jpg)

Moreover, in the response, we also see 2 account entry objects:

- One entry is to record that `10 USD` has been subtracted from account `1`.
- And the other entry is to record that `10 USD` has been added to account `2`.

We can find these 2 records at the end of the `entries` table in the database. Similar for the transfer record at the bottom of the `transfers` table, which matches with the transfer object returned in the JSON response.

Alright, so the transfer API works perfectly. But there’s one thing I want to show you.

## Implement a custom currency validator

Here, in the binding condition of the currency field, we’re hard-coding 3 constants for `USD`, `EUR` and `CAD`.

```go
type transferRequest struct {
    FromAccountID int64  `json:"from_account_id" binding:"required,min=1"`
    ToAccountID   int64  `json:"to_account_id" binding:"required,min=1"`
    Amount        int64  `json:"amount" binding:"required,gt=0"`
    Currency      string `json:"currency" binding:"required,oneof=USD EUR CAD"`
}
```

What if in the future we want to support 100 different types of currency? It would be very hard to read and easy to make mistakes if we put 100 currency values in this oneof tag.

Also, there will be duplications because the currency parameter can appear in many different APIs. For now, we already have 1 duplication in the create account request.

```go
type createAccountRequest struct {
    Owner    string `json:"owner" binding:"required"`
    Currency string `json:"currency" binding:"required,oneof=USD EUR CAD"`
}
```

In order to avoid that, I’m gonna show you how to write a custom validator for the currency field.

Let’s create a new file `validator.go` inside the `api` folder. Then declare a new variable `validCurrency` of type `validator.Func`

```go
package api

import (
    "github.com/go-playground/validator/v10"
    "github.com/techschool/simplebank/util"
)

var validCurrency validator.Func = func(fieldLevel validator.FieldLevel) bool {
    if currency, ok := fieldLevel.Field().Interface().(string); ok {
        return util.IsSupportedCurrency(currency)
    }
    return false
}

```

Visual studio code has automatically imported the [validator](https://github.com/go-playground/validator) package for us. However, we have to add `/v10` at the end of this import path because we want to use the version `10` of this package.

Basically, `validator.Func` is a function that takes a `validator.FieldLevel` interface as input and return `true` when validation succeeds. This is an interface that contains all information and helper functions to validate a field.

What we need to do is calling `fieldLevel.Field()` to get the value of the field. Note that it’s a reflection value, so we have to call `.Interface()` to get its value as an `interface{}`. Then we try to convert this value to a string.

The conversion will return a `currency` string and a `ok` boolean value. If `ok` is `true` then the `currency` is a valid string. In this case, we will have to check if that currency is supported or not. Else, if `ok` is `false`, then the field is not a string. Therefore, we just return `false`.

Alright, now I will create a new file `currency.go` inside the `util` package. We will implement the function `IsSupportedCurrency()` to check if a currency is supported or not in this file.

First I will declare some constants for the currencies that we want to support in our bank. For now let’s say we only support `USD`, `EUR`, and `CAD`. We can always add more currencies in the future if we want.

Then let’s write a new function `IsSupportedCurrency()` that takes a `currency` string as input and return a `bool` value. It will return `true` if the input `currency` is supported and `false` otherwise.

```go
package util

// Constants for all supported currencies
const (
    USD = "USD"
    EUR = "EUR"
CAD = "CAD"
)

// IsSupportedCurrency returns true if the currency is supported
func IsSupportedCurrency(currency string) bool {
    switch currency {
    case USD, EUR, CAD:
        return true
    }
    return false
}

```

In this function, we just use a simple switch case statement. In case the currency is `USD`, `EUR`, or `CAD`, we return `true`. Else, we return `false`.

And that’s it! Our custom validator `validCurrency` is done. Next we have to register this custom validator with [Gin](https://github.com/gin-gonic/gin).

## Register the custom currency validator

Let’s open the `server.go` file.

```go
package api

import (
    "github.com/gin-gonic/gin"
    "github.com/gin-gonic/gin/binding"
    "github.com/go-playground/validator/v10"
)

func NewServer(store db.Store) *Server {
    server := &Server{store: store}
    router := gin.Default()

    if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
        v.RegisterValidation("currency", validCurrency)
    }

    router.POST("/accounts", server.createAccount)
    router.GET("/accounts/:id", server.getAccount)
    router.GET("/accounts", server.listAccounts)

    router.POST("/transfers", server.createTransfer)

    server.router = router
    return server
}
```

Here, after creating the Gin `router`, we call `binding.Validator.Engine()` to get the current validator engine that Gin is using (`binding` is a sub-package of Gin).

Note that this function will return a general interface type, which by default is a pointer to the validator object of the `go-playground/validator/v10` package.

So here we have to convert the output to a `validator.Validate` object pointer. If it is ok then we can call `v.RegisterValidation()` to register our custom validate function.

The first argument is the name of the validation tag: `currency`. And the second argument should be the `validCurrency` function that we have implemented before.

## Use the custom currency validator

Alright, now with this new validator registered, we can start using it.

Here in the `createAccountRequest` struct, we can replace the `oneof=USD EUR CAD` tag with just `currency` tag:

```go
type createAccountRequest struct {
    Owner    string `json:"owner" binding:"required"`
    Currency string `json:"currency" binding:"required,currency"`
}
```

And similar for this `Currency` field of the `transferRequest` struct:

```go
type transferRequest struct {
    FromAccountID int64  `json:"from_account_id" binding:"required,min=1"`
    ToAccountID   int64  `json:"to_account_id" binding:"required,min=1"`
    Amount        int64  `json:"amount" binding:"required,gt=0"`
    Currency      string `json:"currency" binding:"required,currency"`
}
```

OK, let’s restart the server and test it.

```bash
❯ make server
```

I’m gonna change the currency to `EUR` and send the Postman request.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/ejtc72ih2pqs7d1jpguy.jpg)

This is a valid supported currency, but it doesn’t match the currency of the accounts, so we’ve got a `400 Bad Request` status with the `currency mismatch` error.

If I change it back to `USD`, then the request is successful again:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/oeb15z9dx13agizzv2vu.jpg)

Now let’s try an unsupported currency, such as `AUD`:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/x2p91g84u871tc0qq2ih.jpg)

This time, we also get `400 Bad Request` status, but the error is because the field validation for the `currency` failed on the `currency` tag, which is exactly what we expected.

So our custom currency validator is working very well!

So today we have learned how to implement the transfer money API with a custom parameter binding validator. I hope it is useful for you.

Although I didn’t show you how to write unit tests for this new API in the article because it would be very similar to what we’ve learned in the [previous lecture](https://dev.to/techschoolguru/mock-db-for-testing-http-api-in-go-and-achieve-100-coverage-4pa9), I actually still write a lot of unit tests for it and push them to Github.

I encourage you to check them out on the [simple bank repository](https://github.com/techschool/simplebank) to see how they were implemented.

Thanks a lot for reading this article, and I will see you soon in the one!

---

If you like the article, please subscribe to our [Youtube channel](http://bit.ly/techschoolguru) and follow us on [Twitter](https://twitter.com/techschoolguru) or [Facebook](https://www.facebook.com/techschool.guru) for more tutorials in the future.

---
If you want to join me on my current amazing team at Voodoo, check out [our job openings here](https://bit.ly/voodoojobs). Remote or onsite in Paris/Amsterdam/London/Berlin/Barcelona with visa sponsorship.