Client-Server-API with Golang
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:
server.go
should send the dollar quotation toclient.go
- 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 theserver.go
(JSON “bid” field). - The 3 contexts should return an error in the logs if the execution time is insufficient.
- 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 toclient.go
.- Return the result to the client in JSON format.
- The
client.go
will only need to receive the current exchange rate from theserver.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, "e)
if err != nil {
return nil, err
}
return "e, 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:
- The
client.go
should make an HTTP request toserver.go
asking for the dollar exchange rate. - The
client.go
will only need to receive the current exchange rate from theserver.go
(JSON “bid” field). - Using the “context” package,
client.go
will have a maximum timeout of 300ms to receive the result fromserver.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}.
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 tohttp://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 theserver.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
}