In this article we are going to use goroutines and channels to build an API mashup pulling data from two different APIs… This is going to be the result after the mashup of the two APIs that we are going to integrate using goroutines and channels.
{
"id": 1,
"brand": "Mitsubishi",
"model": "Montero",
"model_year": 2002,
"owner_firstname": "Alyosha",
"owner_lastname": "Caldero",
"owner_email": "acaldero0@behance.net"
}
So there are four attributes (identifier, brand, model, and year) are going to be extracted from this endpoint:
$ curl --location 'https://myfakeapi.com/api/cars/1'
{
"Car": {
"id": 1,
"car": "Mitsubishi",
"car_model": "Montero",
"car_color": "Yellow",
"car_model_year": 2002,
"car_vin": "SAJWJ0FF3F8321657",
"price": "$2814.46",
"availability": false
}
}
And there are three attributes (first name, last name, email), we are going to extract those from this endpoint:
curl --location 'https://myfakeapi.com/api/users/1'
{
"User": {
"id": 1,
"first_name": "Alyosha",
"last_name": "Caldero",
"email": "acaldero0@behance.net",
"gender": "Male",
"birthdate": "29/12/1997",
"company_name": "King and Sons",
"department": "Sales",
"job_title": "Senior Editor",
"address": [
{
"street": "1 Hanson Terrace",
"street_name": "Merrick",
"city": "Beaufort",
"state": "South Carolina",
"country": "United States",
"country_code": "US",
"postal_code": "29905"
}
],
"phone": "+7 (835) 885-9702",
"avatar": "https://robohash.org/voluptasautmagni.png?size=180x180&set=set1",
"email_verified": true,
"password": "6707389d040d09a08ad2803846f30db544242f06",
"last_login": "Never",
"last_login_ip": "239.243.71.212",
"subscribed": true
}
}
Preparing HTTP Server
First, we need to define the CarDetails entity that represents the mashup json data. This structure is for the output of the mashup API that we are implementing.
package entity
type CarDetails struct {
ID int `json:"id"`
Brand string `json:"brand"`
Model string `json:"model"`
Year int `json:"model_year"`
FirstName string `json:"owner_firstname"`
LastName string `json:"owner_lastname"`
Email string `json:"owner_email"`
}
Next, we define the service to process the business of getting car detail information and return the CarDetails entity result.
package service
import (
"golang-mux-api/entity"
)
type CarDetailsService interface {
GetDetails() entity.CarDetails
}
type service struct{}
func NewCarDetailsService() CarDetailsService {
return &service{}
}
func (*service) GetDetails() entity.CarDetails {
return entity.CarDetails{
ID: 0,
Brand: "",
Model: "",
Year: 0,
FirstName: "",
LastName: "",
Email: "",
}
}
Then, we define the controller to writing the car details information to the client
package controller
import (
"encoding/json"
"golang-mux-api/service"
"net/http"
)
type controller struct{}
var (
carDetailsService service.CarDetailsService
)
type CarDetailsController interface {
GetCarDetails(response http.ResponseWriter, request *http.Request)
}
func NewCarDetailsController(service service.CarDetailsService) CarDetailsController {
carDetailsService = service
return &controller{}
}
func (*controller) GetCarDetails(response http.ResponseWriter, request *http.Request) {
response.Header().Set("Content-Type", "application/json")
result := carDetailsService.GetDetails()
response.WriteHeader(http.StatusOK)
json.NewEncoder(response).Encode(result)
}
Finaly, we are going to use the HTTP router that we created on a previous article and we are going to add the endpoint that is going to use this resource URI /carDetails
package main
import (
"fmt"
"golang-mux-api/controller"
router "golang-mux-api/http"
"golang-mux-api/service"
"net/http"
)
var (
carDetailsService service.CarDetailsService = service.NewCarDetailsService()
carDetailsController controller.CarDetailsController = controller.NewCarDetailsController(carDetailsService)
httpRouter router.Router = router.NewChiRouter()
)
func main() {
const port string = ":8000"
httpRouter.GET("/", func(resp http.ResponseWriter, request *http.Request) {
fmt.Fprint(resp, "Up and running...")
})
httpRouter.GET("/carDetails", carDetailsController.GetCarDetails)
httpRouter.SERVE(port)
}
Now we can build and test our very first simple REST API to return car details information
$ go build
$ go run .
$ curl --location 'http://localhost:8000/carDetails'
{
"id": 0,
"brand": "",
"model": "",
"model_year": 0,
"owner_firstname": "",
"owner_lastname": "",
"owner_email": ""
}
Define structs for extracted data
First let’s start working on four attributes (id, brand, model, year) that are part of the first API, so we are going to create new file here this is going to be car.go in entity package. It will have Car struct that is the wrapper of CarData struct with those four attributes.
package entity
type Car struct {
CarData `json:"Car"`
}
type CarData struct {
ID int `json:"id"`
Brand string `json:"car"`
Model string `json:"car_model"`
Year int `json:"car_model_year"`
}
Next, let’s create the structure that is going to represent the second API. So let’s create new file and call it owner.go in entity package.
package entity
type Owner struct {
OwnerData `json:"User"`
}
type OwnerData struct {
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Email string `json:"email"`
}
Setup Goroutines to get data
We have two goroutines to get data from two external APIs, in GetDetails method of CarDetailsService in file car-details-service.go . And we are going to create two channels (userChannel and carChannel) to get data. Goroutines are actually going to call two endpoints those are going to be 2 asynchronous operations that we want to create a (to run on a new) thread so they can run in the background and once we get the results we are going to receive that results here in the channels that we are going to create.
Let’s create a service to call the endpoint 1, so this is going to be the CarService in the file car-service.go
package service
import (
"fmt"
"net/http"
)
type CarService interface {
FetchData()
}
const (
carServiceUrl = "https://myfakeapi.com/api/cars/1"
)
type fetchCarDataService struct{}
func NewCarService() CarService {
return &fetchCarDataService{}
}
func (*fetchCarDataService) FetchData() {
client := http.Client{}
fmt.Printf("Fetching the url %s", carServiceUrl)
// Call the external API
resp, _ := client.Get(carServiceUrl)
// Write response to the channel (TODO)
carDataChannel <- resp
}
Similar to this one it’s going to be the OwnerService in the file owner-service.go .
package service
import (
"fmt"
"net/http"
)
type OwnerService interface {
FetchData()
}
const (
ownerServiceUrl = "https://myfakeapi.com/api/users/1"
)
type fetchOwnerDataService struct{}
func NewOwnerService() OwnerService {
return &fetchOwnerDataService{}
}
func (*fetchOwnerDataService) FetchData() {
client := http.Client{}
fmt.Printf("Fetching the url %s", ownerServiceUrl)
// Call the external API
resp, _ := client.Get(ownerServiceUrl)
// Write response to the channel (TODO)
ownerDataChannel <- resp
}
No we need use those services for the CarDetailsService in the file car-details-service.go
package service
import (
"encoding/json"
"fmt"
"golang-mux-api/entity"
"net/http"
)
type CarDetailsService interface {
GetDetails() entity.CarDetails
}
var (
carService CarService = NewCarService()
ownerService OwnerService = NewOwnerService()
carDataChannel = make(chan *http.Response)
ownerDataChannel = make(chan *http.Response)
)
type serviceCarDetails struct{}
func NewCarDetailsService() CarDetailsService {
return &serviceCarDetails{}
}
func (*serviceCarDetails) GetDetails() entity.CarDetails {
// goroutine get data from endpoint 1: https://myfakeapi.com/api/cars/1
go carService.FetchData()
// goroutine get data from endpoint 2: https://myfakeapi.com/api/users/1
go ownerService.FetchData()
// create carChannel to get the data from endpoint 1
car, _ := getCarData()
// create ownerChannel to get the data from endpoint 2
owner, _ := getOwnerData()
return entity.CarDetails{
ID: car.ID,
Brand: car.Brand,
Model: car.Model,
Year: car.Year,
FirstName: owner.FirstName,
LastName: owner.LastName,
Email: owner.Email,
}
}
func getCarData() (entity.Car, error) {
r1 := <-carDataChannel
var car entity.Car
err := json.NewDecoder(r1.Body).Decode(&car)
if err != nil {
fmt.Print(err.Error())
return car, err
}
return car, nil
}
func getOwnerData() (entity.Owner, error) {
r1 := <-ownerDataChannel
var owner entity.Owner
err := json.NewDecoder(r1.Body).Decode(&owner)
if err != nil {
fmt.Print(err.Error())
return owner, err
}
return owner, nil
}
Create Unit Test for service
Now it should be OK then we are going to create a test for our CarDetailsService in the file car-details-service_test.go .
package service
import (
"testing"
"github.com/stretchr/testify/assert"
)
var (
carDetailsService = NewCarDetailsService()
)
func TestGetDetails(t *testing.T) {
carDetails := carDetailsService.GetDetails()
assert.NotNil(t, carDetails)
assert.Equal(t, 1, carDetails.ID)
assert.Equal(t, "Mitsubishi", carDetails.Brand)
assert.Equal(t, 2002, carDetails.Year)
assert.Equal(t, "Alyosha", carDetails.FirstName)
}
Let’s run the test
$ cd service/
$ go test -run TestGetDetails
Fetching the url https://myfakeapi.com/api/users/1
Fetching the url https://myfakeapi.com/api/cars/1
PASS
ok golang-mux-api/service 1.330s
Run and test the REST API
Let’s run by comand:
$ go run *.go
Chi HTTP server running on port :8000
Test the REST API
$ curl -v --location 'http://localhost:8000/carDetails'
* Trying 127.0.0.1:8000...
* Connected to localhost (::1) port 8000 (#0)
> GET /carDetails HTTP/1.1
> Host: localhost:8000
< HTTP/1.1 200 OK
< Content-Type: application/json
<
[159 bytes data]
{"id":1,"brand":"Mitsubishi","model":"Montero","model_year":2002,"owner_firstname":"Alyosha","owner_lastname":"Caldero","owner_email":"acaldero0@behance.net"}
Download Source Code
$ git clone https://github.com/favtuts/golang-mux-api.git
$ cd golang-mux-api$ git checkout goroutines-channels
$ go build$ go run .