Invoking the API via a client library

The social graph manager is now accessible through an HTTP REST API. Here is a quick local demo. First, I will launch the Postgres DB (I have a Docker image called postgres), which is used as the data store, and then I will run the service itself in the service directory, that is, delinkcious/svc/social_graph_service:

$ docker restart postgres
$ go run main.go

2018/12/31 10:41:23 Listening on port 9090...

Let's add a couple of follower/following relationships by invoking the /follow endpoint. I will use the excellent HTTPie (https://httpie.org/), which is a better curl in my honest opinion. However, you can use curl if you prefer:

$ http POST http://localhost:9090/follow followed=liat follower=gigi
HTTP/1.1 200 OK
Content-Length: 11
Content-Type: text/plain; charset=utf-8
Date: Mon, 31 Dec 2018 09:19:01 GMT

{
"err": ""
}

$ http POST http://localhost:9090/follow followed=guy follower=gigi
HTTP/1.1 200 OK
Content-Length: 11
Content-Type: text/plain; charset=utf-8
Date: Mon, 31 Dec 2018 09:19:01 GMT

{
"err": ""
}

These two calls made the gigi user follow the liat and guy users. Let's use the /following endpoint to verify this:

$ http GET http://localhost:9090/following/gigi
HTTP/1.1 200 OK
Content-Length: 37
Content-Type: text/plain; charset=utf-8
Date: Mon, 31 Dec 2018 09:37:21 GMT

{
"err": "",
"following": {
"guy": true
"liat": true
}
}

The JSON response has an empty error, and the following map contains the guy and liat users, as expected.

While a REST API is cool, we can do better. Instead of forcing the caller to understand the URL schema of our service and decode and encode JSON payloads, why not provide a client library that does all of that? This is especially true for internal microservices that all talk to each other using a small number of languages, and in many cases, just one language. The service and client can share the same interface and, maybe even some common types. In addition, Go kit provides support for client-side endpoints that are pretty similar to service-side endpoints. This translates directly into a very streamlined end-to-end developer experience, where you just stay in the programming language space. All the endpoints, transports, encoding, and decoding can remain hidden as an implementation detail for the most part.

The social graph service provides a client library that lives in the pkg/social_graph_client package. The client.go file is similar to the social_graph_service.go file and is responsible for creating a set of endpoints in the NewClient() function and returning the SocialGraphManager interface. The NewClient() function takes the base URL as an argument and then constructs a set of client endpoints using Go kit's NewClient() function of the HTTP transport. Each endpoint requires a URL, a method (GET or POST, in this case), a request encoder, and a response decoder. It's like a mirror image of the service. Then, it assigns the client endpoints to the EndpointSet struct, which can expose them through the SocialGraphManager interface:

func NewClient(baseURL string) (om.SocialGraphManager, error) {
// Quickly sanitize the instance string.
if !strings.HasPrefix(baseURL, "http") {
baseURL = "http://" + baseURL
}
u, err := url.Parse(baseURL)
if err != nil {
return nil, err
}

followEndpoint := httptransport.NewClient(
"POST",
copyURL(u, "/follow"),
encodeHTTPGenericRequest,
decodeSimpleResponse).Endpoint()

unfollowEndpoint := httptransport.NewClient(
"POST",
copyURL(u, "/unfollow"),
encodeHTTPGenericRequest,
decodeSimpleResponse).Endpoint()

getFollowingEndpoint := httptransport.NewClient(
"GET",
copyURL(u, "/following"),
encodeGetByUsernameRequest,
decodeGetFollowingResponse).Endpoint()

getFollowersEndpoint := httptransport.NewClient(
"GET",
copyURL(u, "/followers"),
encodeGetByUsernameRequest,
decodeGetFollowersResponse).Endpoint()

// Returning the EndpointSet as an interface relies on the
// EndpointSet implementing the Service methods. That's just a simple bit
// of glue code.
return EndpointSet{
FollowEndpoint: followEndpoint,
UnfollowEndpoint: unfollowEndpoint,
GetFollowingEndpoint: getFollowingEndpoint,
GetFollowersEndpoint: getFollowersEndpoint,
}, nil
}

The EndpointSet struct is defined in the endpoints.go file. It contains the endpoints themselves, which are functions, and it implements the SocialGraphManager method, where it delegates the work to the endpoint's functions:

type EndpointSet struct {
FollowEndpoint endpoint.Endpoint
UnfollowEndpoint endpoint.Endpoint
GetFollowingEndpoint endpoint.Endpoint
GetFollowersEndpoint endpoint.Endpoint
}

Let's examine the EndpointSet struct's GetFollowing() method. It accepts the username as a string, and then it calls the endpoint with a getByUserNameRequest that's populated with the input username. If calling the endpoint function returned an error, it just bails out. Otherwise, it does type assertion to convert the generic response into a getFollowingResponse struct. If its error string wasn't empty, it creates a Go error from it. Eventually, it returns the following users from the response as a map:

func (s EndpointSet) GetFollowing(username string) (following map[string]bool, err error) {
resp, err := s.GetFollowingEndpoint(context.Background(), getByUserNameRequest{Username: username})
if err != nil {
return
}

response := resp.(getFollowingResponse)
if response.Err != "" {
err = errors.New(response.Err)
}
following = response.Following
return
}