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 .

Leave a Reply

Your email address will not be published. Required fields are marked *