Declarative HTTP Client Generator for Go

Generate HTTP clients from simple Go interface definitions with annotations

HTTP
Declarative
Code Generation
todo.go
package api

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

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

// @BaseURL("https://jsonplaceholder.typicode.com")
type TodoAPI interface {
    genapi.Interface

    // @GET("/todos")
    GetTodos(ctx context.Context) ([]Todo, error)

    // @GET("/todos/{id}")
    GetTodo(ctx context.Context, id string) (Todo, error)
    
    // @POST("/todos")
    CreateTodo(ctx context.Context, todo Todo) (Todo, error)
}

Examples

package api

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

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

// @BaseURL("https://jsonplaceholder.typicode.com")
type TodoAPI interface {
    genapi.Interface

    // @GET("/todos")
    GetTodos(ctx context.Context) ([]Todo, error)

    // @GET("/todos/{id}")
    GetTodo(ctx context.Context, id string) (Todo, error)

    // @POST("/todos")
    CreateTodo(ctx context.Context, todo Todo) (Todo, error)

    // @PUT("/todos/{id}")
    UpdateTodo(ctx context.Context, id string, todo Todo) (Todo, error)

    // @DELETE("/todos/{id}")
    DeleteTodo(ctx context.Context, id string) error
}

type Todo struct {
    UserID    int    `json:"userId"`
    ID        int    `json:"id"`
    Title     string `json:"title"`
    Completed bool   `json:"completed"`
}
// CODE GENERATED BY genapi. DO NOT EDIT.
package api

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

type implTodoAPI struct {
    client genapi.HttpClient
}

// SetHttpClient implments genapi.Interface
func (i *implTodoAPI) SetHttpClient(client genapi.HttpClient) {
    i.client = client
}

func (i *implTodoAPI) GetTodos(ctx context.Context) ([]Todo, error) {
    resp, err := i.client.Do(&genapi.Request{
        Method:  "GET",
        Path:    "/todos",
        Context: ctx,
    })
    return genapi.HandleResponse[[]Todo](resp, err)
}

func (i *implTodoAPI) GetTodo(ctx context.Context, id string) (Todo, error) {
    resp, err := i.client.Do(&genapi.Request{
        Method:     "GET",
        Path:       "/todos/{id}",
        PathParams: map[string]string{"id": id},
        Context:    ctx,
    })
    return genapi.HandleResponse[Todo](resp, err)
}

func (i *implTodoAPI) CreateTodo(ctx context.Context, todo Todo) (Todo, error) {
    resp, err := i.client.Do(&genapi.Request{
        Method:  "POST",
        Path:    "/todos",
        Context: ctx,
        Body:    todo,
    })
    return genapi.HandleResponse[Todo](resp, err)
}
package main

import (
    "context"
    "fmt"
    "log"
    
    "yourproject/api"
)

func main() {
    // Get API client instance
    client := genapi.New[api.TodoAPI]()
    
    // Fetch all todos
    todos, err := client.GetTodos(context.Background())
    if err != nil {
        log.Fatalf("Error fetching todos: %v", err)
    }
    
    fmt.Printf("Found %d todos\n", len(todos))
    
    // Create a new todo
    newTodo := api.Todo{
        UserID:    1,
        Title:     "Learn genapi",
        Completed: false,
    }
    
    created, err := client.CreateTodo(context.Background(), newTodo)
    if err != nil {
        log.Fatalf("Error creating todo: %v", err)
    }
    
    fmt.Printf("Created todo with ID: %d\n", created.ID)
}

Why genapi?

Less Boilerplate

Focus on defining your API interface, not implementing repetitive HTTP client code

Type Safety

Catch errors at compile time with Go's static type system. No more runtime surprises.

Clean Architecture

Clear separation of interface and implementation for more maintainable codebases

Code Generation

Automatic generation of client implementation code that follows best practices

Get started

1

Add genapi to your project

$ go get github.com/lexcao/genapi
2

Define Your API Interface

Create a Go interface with annotations that describe your API endpoints

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

// @BaseURL("https://api.example.com")
type UserAPI interface {
    genapi.Interface
    
    // @GET("/users")
    GetUsers(ctx context.Context) ([]User, error)
}
3

Run Code Generation

Use the genapi tool to generate implementation code from your interface

$ go generate ./...
4

Use Your Generated Client

Instantiate and use your type-safe client in your application

import "github.com/lexcao/genapi"

// Create client
client := genapi.New[api.UserAPI]()

// Use client with type safety
users, err := client.GetUsers(context.Background())

Annotation Reference

Base Configuration

Configure the base settings for your API client

@BaseURL

Sets the base URL for all API endpoints. Must be set at the interface level.

Interface-level Optional
Example
// @BaseURL("https://api.example.com/v1")
type ExampleAPI interface {
    genapi.Interface
    
    // Methods...
}

HTTP Methods

Define HTTP method and path for endpoints

@GET, @POST, @PUT, @DELETE, @PATCH

Specify the HTTP method and path for an API endpoint.

Method-level Required
Example
// @GET("/users")
GetUsers(ctx context.Context) ([]User, error)

// @POST("/users")
CreateUser(ctx context.Context, user User) (User, error)

// @PUT("/users/{id}")
UpdateUser(ctx context.Context, id string, user User) (User, error)

// @DELETE("/users/{id}")
DeleteUser(ctx context.Context, id string) error

// @PATCH("/users/{id}/status")
UpdateUserStatus(ctx context.Context, id string, status UserStatus) error

Parameters

Working with URL parameters, query parameters, and paths

Path Parameters

Parameters enclosed in curly braces in the URL path are automatically mapped to method arguments with the same name.

Path-based
Example
// @GET("/users/{id}/posts/{postId}")
GetUserPost(ctx context.Context, id string, postId int) (Post, error)

@Query

Use @Query to define query parameters for an endpoint.

Parameter-level Optional
Example
// @GET("/users")
// @Query("page", "limit", "sort")
GetUsers(ctx context.Context, page int, limit int, sort string) ([]User, error)

Request Body

Handling request payloads and serialization

Automatic Body Serialization

Complex types after the context parameter are automatically serialized as JSON request bodies.

Convention-based
Example
// @POST("/users")
CreateUser(ctx context.Context, user User) (User, error)

type User struct {
    ID        string `json:"id,omitempty"`
    Name      string `json:"name"`
    Email     string `json:"email"`
    CreatedAt string `json:"created_at,omitempty"`
}

Response Handling

Processing API responses and handling errors

Automatic Response Deserialization

Return types are automatically deserialized from JSON API responses.

Convention-based
Example
// No response body
func DeleteUser(ctx context.Context, id string) error

// API returns a User object as JSON, automatically deserialized to the User struct
// Typed response with error handling
func GetUser(ctx context.Context, id string) (User, error)

// You can specify the raw genapi.Response as well
// Raw response access
func GetRawResponse(ctx context.Context) (*genapi.Response, error)

// Must-style response (panics on error)
func MustGetUser(ctx context.Context, id string) User

Headers & Auth

Working with HTTP headers and authentication

@Header

Define custom HTTP headers for requests.

Interface-level Method-level Optional
Example
// Header("User-Agent", "genapi")
type SomeAPI interface {
    genapi.Interface
    
    // @GET("/special-endpoint")
    // @Header("X-Custom-Header: custom-value")
    // @Header("X-Api-Version: 2")
    GetSpecialData(ctx context.Context) (SpecialData, error)
}