Challenge Description

In this challenge we’re going to apply what we’ve learnt about http webserver, contexts, database and file manipulation with Go.

You’ll need to deliver two Go systems:

  • client.go
  • server.go

The requirements to fulfil this challenge are:

The client.go should make an HTTP request to server.go asking for the dollar exchange rate.

The server.go must consume the API containing the Dollar/Real exchange rate at the address: https://economia.awesomeapi.com.br/json/last/USD-BRL and then return the result to the client in JSON format.

Using the “context” package, server.go should record each quotation received in the SQLite database, with the maximum timeout for calling the dollar quote API being 200ms and the maximum timeout for persisting the data in the database being 10ms.

The client.go will only need to receive the current exchange rate from the server.go (JSON “bid” field). Using the “context” package, client.go will have a maximum timeout of 300ms to receive the result from server.go.

The 3 contexts should return an error in the logs if the execution time is insufficient.

The client.go will have to save the current exchange rate in a “cotacao.txt” file in the following format: Dólar: {value}

The necessary endpoint generated by server.go for this challenge will be: /cotacao and the port to be used by the HTTP server will be 8080.

When finalised, send the link to the repository for correction.

Challenge Solution Development

Project structure

Create two folders one for the “server” where the server.go file will be and other called “client” for the client.go file. Both will have the main package and they will start like this:

package main

func main(){

}

Since we will only use some external packages to develop this project we will need to create a module.

To do this open a cmd where your project is and do go mod init <module name>. The parameter <module name> can be any string you like, it will act as an unique identifier to the module, but as good practice we use the URL path where the code will be hosted.

e.g.: go mod init github.com/kelwynOliveira/pg-go-expert/05-Database/20-Client-Server-API-challenge

We also will be using docker so we will add this at the same level as our go.mod:

File docker-compose.yaml

version: "3"

services:
  mysql:
    image: mysql:5.7
    container_name: mysql
    restart: always
    platform: linux/amd64
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: challenge
      MYSQL_PASSWORD: root
    ports:
      - 3306:3306

The project structure will then be:

.
├── Project
│ └── client
│  └── client.go
│ └── server
│  └── server.go
└── docker-compose.yaml
└── go.mod

The Server System

According to challenge’s description the requirements for the server system are:

  1. server.go should send the dollar quotation to client.go
  2. The server.go must consume the API containing the Dollar/Real exchange rate at the address: https://economia.awesomeapi.com.br/json/last/USD-BRL and then return the result to the client in JSON format.
  3. Using the “context” package, server.go should record each quotation received in the SQLite database, with the maximum timeout for calling the dollar quote API being 200ms and the maximum timeout for persisting the data in the database being 10ms.
  4. The client.go will only need to receive the current exchange rate from the server.go (JSON “bid” field).
  5. The 3 contexts should return an error in the logs if the execution time is insufficient.
  6. The necessary endpoint generated by server.go for this challenge will be: /cotacao and the port to be used by the HTTP server will be 8080.

Creating the server

To develop this we must have in mind that:

  • The necessary endpoint generated by server.go for this challenge will be: /cotacao and the port to be used by the HTTP server will be 8080.
func main() {
	http.HandleFunc("/cotacao", HandleQuote)
	http.ListenAndServe(":8080", nil)
}

The function HandleQuote

To develop the HandleQuote function we must have in mind that:

  • server.go must consume the API containing the Dollar/Real exchange rate.
  • server.go should record each quotation received in the SQLite database.
  • The contexts should return an error in the logs if the execution time is insufficient.
  • server.go should send the dollar quotation to client.go.
  • Return the result to the client in JSON format.
  • The client.go will only need to receive the current exchange rate from the server.go (JSON “bid” field).
func HandleQuote(w http.ResponseWriter, r *http.Request) {
	if r.URL.Path != "/cotacao" {
		w.WriteHeader(http.StatusNotFound)
		return
	}

  // Look into API
	quotation, err := SearchUSDBRL()
	if err != nil {
		log.Println(err)
		w.WriteHeader(http.StatusInternalServerError)
		return
	}

  // Saves on DataBase
	err = DataBase(quotation)
	if err != nil {
		log.Println(err)
		w.WriteHeader(http.StatusBadRequest)
		return
	}

  // Returns the Bid in json format
	var bid Price
	bid.Bid = quotation.USDBRL.Bid
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusOK)
	json.NewEncoder(w).Encode(bid)
}

Struct to save the bid value

type Price struct {
	Bid string `json:"bid"`
}

Searching the exchange rate

To develop this part we must have in mind that:

  • The API containing the Dollar/Real exchange rate is at the address: https://economia.awesomeapi.com.br/json/last/USD-BRL.
  • The maximum timeout for calling the dollar quote API of 200ms (use context package).

Structs to receive the json from API

type QuoteAPI struct {
	USDBRL Quote `json:"USDBRL"`
}

type Quote struct {
	Code       string `json:"code"`
	Codein     string `json:"codein"`
	Name       string `json:"name"`
	High       string `json:"high"`
	Low        string `json:"low"`
	VarBid     string `json:"varBid"`
	PctChange  string `json:"pctChange"`
	Bid        string `json:"bid"`
	Ask        string `json:"ask"`
	Timestamp  string `json:"timestamp"`
	CreateDate string `json:"create_date"`
}

Function to Search the Bid value

func SearchUSDBRL() (*QuoteAPI, error) {
	ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
	defer cancel()

	request, err := http.NewRequestWithContext(ctx, "GET", "https://economia.awesomeapi.com.br/json/last/USD-BRL", nil)
	if err != nil {
		return nil, err
	}

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

	result, err := io.ReadAll(response.Body)
	if err != nil {
		return nil, err
	}

	var quote QuoteAPI
	err = json.Unmarshal(result, &quote)
	if err != nil {
		return nil, err
	}

	return &quote, nil
}

Saving on Database

To develop this part odf the project we must have in mind that:

  • server.go should record each quotation received in the SQLite database, with the maximum timeout for persisting the data in the database being 10ms (using context).

Import "gorm.io/driver/sqlite" and "gorm.io/gorm"

func NewQuote(quote *QuoteAPI) *Quote {
	return &Quote{
		Code:       quote.USDBRL.Code,
		Codein:     quote.USDBRL.Codein,
		Name:       quote.USDBRL.Name,
		High:       quote.USDBRL.High,
		Low:        quote.USDBRL.Low,
		VarBid:     quote.USDBRL.VarBid,
		PctChange:  quote.USDBRL.PctChange,
		Bid:        quote.USDBRL.Bid,
		Ask:        quote.USDBRL.Ask,
		Timestamp:  quote.USDBRL.Timestamp,
		CreateDate: quote.USDBRL.CreateDate,
	}
}

func DataBase(quote *QuoteAPI) error {
	db, err := gorm.Open(sqlite.Open("./quotes.db"), &gorm.Config{})
	if err != nil {
		panic(err)
	}
	db.AutoMigrate(&Quote{})

	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
	defer cancel()

	usdbrl := NewQuote(quote)

	return db.WithContext(ctx).Create(&usdbrl).Error
}

The Client System

According to challenge’s description the requirements for the client system are:

  1. The client.go should make an HTTP request to server.go asking for the dollar exchange rate.
  2. The client.go will only need to receive the current exchange rate from the server.go (JSON “bid” field).
  3. Using the “context” package, client.go will have a maximum timeout of 300ms to receive the result from server.go.
  4. The 3 contexts should return an error in the logs if the execution time is insufficient.
  5. The client.go will have to save the current exchange rate in a “cotacao.txt” file in the following format: Dólar: {value}.
Making the http request to server.go

To develop this request we must have in mind that:

  • The request is made to server.go in port 8080 with endpoint in /cotacao, so the request is made to http://localhost:8080/cotacao.
  • We must made the request in a context with maximum timeout of 300ms to receive the result from server.go.
  • The context returns an error if the time is insufficient.
func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 300*time.Millisecond)
	defer cancel()

	request, err := http.NewRequestWithContext(ctx, "GET", "http://localhost:8080/cotacao", nil)
	if err != nil {
		log.Println(err)
	}

	response, err := http.DefaultClient.Do(request)
	if err != nil {
		log.Println(err)
	}
	defer response.Body.Close()

	result, err := io.ReadAll(response.Body)
	if err != nil {
		log.Println(err)
	}

  // Save value in file
	err = SaveInFile(result)
	if err != nil {
		log.Println(err)
	}
}

Saving the value

To develop the save action we must have in mind that:

  • The client.go will receive the current exchange rate from the server.go (JSON “bid” field) in a json format.
  • The client.go will have to save the current exchange rate in a “cotacao.txt” file in the following format: Dólar: {value}.

Struct to receive the json from server.go

type Price struct {
	Bid string `json: bid`
}

The SaveInFile dunction

func SaveInFile(result []byte) error {
	var price Price
	err := json.Unmarshal(result, &price)
	if err != nil {
		return err
	}

	file, err := os.Create("cotacao.txt")
	if err != nil {
		return err
	}
	defer file.Close()

  // Write in the file
	size, err := fmt.Fprintln(file, "Dólar:", price.Bid)
	if err != nil {
		return err
	}
	fmt.Printf("File created with success! Size: %d bytes\n", size)

	return nil
}