In this article, I’m excited to introduce genapi, a code generator for Golang HTTP clients. For comprehensive documentation and implementation details, you can explore the genapi website or check out our GitHub repository.

From Manual to Automatic: Evolution of Golang HTTP Client

In Golang development, making HTTP API calls is a very common requirement. Through a weather API example, this article will demonstrate how HTTP client code evolves from manual writing to automatic generation. Let’s look at this simple weather API:

GET /api/weather?city=shanghai
Response:
{
    "temperature": 25,
    "humidity": 60,
    "condition": "sunny"
}

Initial Manual Approach

Initially, we might write code directly like this:

func getWeather(city string) (*Weather, error) {
    resp, err := http.Get("https://api.weather.com/api/weather?city=" + city)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()
    
    var weather Weather
    if err := json.NewDecoder(resp.Body).Decode(&weather); err != nil {
        return nil, err
    }
    
    return &weather, nil
}

This approach is straightforward but has several issues:

  1. Hardcoded URLs in the code
  2. Error-prone parameter concatenation
  3. Duplicate error handling logic
  4. Repetitive response parsing code

Templated Requests

To address these issues, we begin abstracting and templatizing the code:

type Client struct {
    baseURL string
    client  *http.Client
}

func (c *Client) doRequest(method, path string, query url.Values, result interface{}) error {
    u, _ := url.Parse(c.baseURL + path)
    u.RawQuery = query.Encode()
    
    req, err := http.NewRequest(method, u.String(), nil)
    if err != nil {
        return err
    }
    
    resp, err := c.client.Do(req)
    if err != nil {
        return err
    }
    defer resp.Body.Close()
    
    return json.NewDecoder(resp.Body).Decode(result)
}

func (c *Client) GetWeather(city string) (*Weather, error) {
    query := url.Values{}
    query.Set("city", city)
    
    var weather Weather
    err := c.doRequest("GET", "/api/weather", query, &weather)
    return &weather, err
}

These improvements bring several benefits:

  1. Unified error handling mechanism
  2. Safer parameter construction
  3. Reusable request handling logic

However, we still need to manually write each API method.

genapi: Annotation-Driven Code Generation

To further improve development efficiency, introducing genapi. Through simple annotations, we can automatically generate all API call code:

package api

import "github.com/lexcao/genapi"

//go:generate go run github.com/lexcao/genapi/cmd/genapi -file $GOFILE

// WeatherAPI defines the weather service API
// @BaseURL("https://api.weather.com")
type WeatherAPI interface {
    genapi.Interface

    // @get("/api/weather")
    // @query("city", "{city}")
    GetWeather(ctx context.Context, city string) (*Weather, error)
}

By just defining the interface and adding annotations, genapi will automatically generate the complete client code using go generate:

// CODE GENERATED BY genapi. DO NOT EDIT.
package api

import (
    "context"
    "github.com/lexcao/genapi"
    "net/url"
)

type implWeatherAPI struct {
    client genapi.HttpClient
}

// SetHttpClient implements genapi.Interface
func (i *implWeatherAPI) SetHttpClient(client genapi.HttpClient) {
    i.client = client
}

func (i *implWeatherAPI) GetWeather(ctx context.Context, city string) (*Weather, error) {
    resp, err := i.client.Do(&genapi.Request{
        Method: "get",
        Path:   "/api/weather",
        Queries: url.Values{
            "city": []string{
                city,
            },
        },
        Context: ctx,
    })
    return genapi.HandleResponse[*Weather](resp, err)
}

func init() {
    genapi.Register[WeatherAPI, *implWeatherAPI](
        genapi.Config{
            BaseURL: "https://api.weather.com",
        },
    )
}

Using the Generated Code

func main() {
    client := genapi.New[api.WeatherAPI]()

    weather, err := client.GetWeather(context.TODO(), "shanghai")
}

Replacing Different HttpClients

One of the core features of genapi is its support for dynamically replacing HttpClient at runtime. This gives us great flexibility to switch between different HTTP client implementations as needed.

Replacing the Default HttpClient

import (
    http_client "net/http"
    "github.com/lexcao/genapi"
    "github.com/lexcao/genapi/pkg/clients/http"
)

func main() {
    httpClient := &http_client.Client{}
    
    // Specify when creating
    client := genapi.New[api.WeatherAPI](
        genapi.WithHttpClient(http.New(httpClient))
    )

    // Or set at runtime
    client.SetHttpClient(httpClient)
}

Using Resty

genapi has built-in support for Resty client. First, you need to install:

go get github.com/lexcao/genapi/pkg/clients/resty

Then you can use it like this:

import (
    "github.com/lexcao/genapi"
    "github.com/lexcao/genapi/pkg/clients/resty"
    resty_client "github.com/go-resty/resty/v2"
)

func main() {
    client := genapi.New[api.WeatherAPI](
        genapi.WithHttpClient(resty.DefaultClient),           // Use default configuration
        genapi.WithHttpClient(resty.New(resty_client.New())), // Custom configuration
    )
}

Implementing Your Own HttpClient

You can also implement your own HttpClient by implementing the genapi.HttpClient interface:

type HttpClient interface {
    SetConfig(Config)
    Do(req *Request) (*Response, error)
}

You can also use the test suite genapi.TestHttpClient to verify if your implementation covers the basic use cases:

Summary

Through annotation-driven approach, genapi enables developers to:

  • Focus on interface definition, avoiding repetitive code
  • Improve development efficiency and reduce maintenance costs
  • Make code clearer and more reliable
  • Dynamically replace HttpClient implementations