Skip to content

Instantly share code, notes, and snippets.

@aasumitro
Last active February 10, 2026 07:30
Show Gist options
  • Select an option

  • Save aasumitro/2459aebf1b66c6f884fb0461c8aba3a6 to your computer and use it in GitHub Desktop.

Select an option

Save aasumitro/2459aebf1b66c6f884fb0461c8aba3a6 to your computer and use it in GitHub Desktop.
REST Repo

REST Repository

How to Use

import (
    "fmt"
	
    restRepo "cooperative.bakode.xyz/internal/repository/rest"
)

const baseURL := "http://test.api/v1"
rest := restRepo.New(baseURL)

resp, err :=  rest.Post(context.Background(), "", rest.WithBody(body),
    rest.WithHeader("Content-Type", "application/json"))

fmt.Println(resp, err)
package rest
import (
"bytes"
"context"
"fmt"
"io"
"net/http"
"time"
"go.opentelemetry.io/otel"
)
type (
IRESTRepository interface {
Get(ctx context.Context, endpoint string, opts ...Option) (*Response, error)
Post(ctx context.Context, endpoint string, opts ...Option) (*Response, error)
Put(ctx context.Context, endpoint string, opts ...Option) (*Response, error)
Patch(ctx context.Context, endpoint string, opts ...Option) (*Response, error)
Delete(ctx context.Context, endpoint string, opts ...Option) (*Response, error)
}
// Client is a struct who has baseURL property
Client struct {
baseURL string
httpClient *http.Client
headers map[string]Header
query map[string]string
body []byte
timeout time.Duration
}
Header struct {
Value string
IsDefault bool
}
)
const (
DefaultTimeout = 10 * time.Second
namespace = "cooperative.bakode.xyz/internal/repository/rest"
)
var tracer = otel.Tracer(namespace)
// New func returns a Client struct
func New(baseURL string, opts ...ClientOption) *Client {
httpClient := &http.Client{Timeout: DefaultTimeout}
client := &Client{httpClient: httpClient, baseURL: baseURL, timeout: DefaultTimeout}
for _, opt := range opts {
opt(client)
}
return client
}
// Get func returns a request
func (c *Client) Get(ctx context.Context, endpoint string, opts ...Option) (*Response, error) {
closeOption := c.initOpts(opts...)
defer closeOption()
req, err := http.NewRequestWithContext(ctx,
http.MethodGet, c.baseURL+endpoint, http.NoBody)
if err != nil {
return nil, err
}
prepReq := c.prepareReq(req)
return c.sendReq(ctx, prepReq)
}
// Post func returns a request
func (c *Client) Post(ctx context.Context, endpoint string, opts ...Option) (*Response, error) {
closeOption := c.initOpts(opts...)
defer closeOption()
req, err := http.NewRequestWithContext(ctx, http.MethodPost,
c.baseURL+endpoint, bytes.NewBuffer(c.body))
if err != nil {
return nil, err
}
prepReq := c.prepareReq(req)
return c.sendReq(ctx, prepReq)
}
// Put func returns a request
func (c *Client) Put(ctx context.Context, endpoint string, opts ...Option) (*Response, error) {
closeOption := c.initOpts(opts...)
defer closeOption()
req, err := http.NewRequestWithContext(ctx, http.MethodPut,
c.baseURL+endpoint, bytes.NewBuffer(c.body))
if err != nil {
return nil, err
}
prepReq := c.prepareReq(req)
return c.sendReq(ctx, prepReq)
}
// Patch func returns a request
func (c *Client) Patch(ctx context.Context, endpoint string, opts ...Option) (*Response, error) {
closeOption := c.initOpts(opts...)
defer closeOption()
req, err := http.NewRequestWithContext(ctx, http.MethodPatch,
c.baseURL+endpoint, bytes.NewBuffer(c.body))
if err != nil {
return nil, err
}
prepReq := c.prepareReq(req)
return c.sendReq(ctx, prepReq)
}
// Delete func returns a request
func (c *Client) Delete(ctx context.Context, endpoint string, opts ...Option) (*Response, error) {
closeOption := c.initOpts(opts...)
defer closeOption()
req, err := http.NewRequestWithContext(ctx, http.MethodDelete,
c.baseURL+endpoint, bytes.NewBuffer(c.body))
if err != nil {
return nil, err
}
prepReq := c.prepareReq(req)
return c.sendReq(ctx, prepReq)
}
func (c *Client) initOpts(opts ...Option) func() {
for _, opt := range opts {
opt(c)
}
return func() {
for key, header := range c.headers {
if !header.IsDefault {
delete(c.headers, key)
}
}
c.query = make(map[string]string)
c.body = nil
}
}
func (c *Client) prepareReq(req *http.Request) *http.Request {
// set headers
for key, header := range c.headers {
req.Header.Set(key, header.Value)
}
// set query
q := req.URL.Query()
for key, value := range c.query {
q.Add(key, value)
}
req.URL.RawQuery = q.Encode()
return req
}
func (c *Client) sendReq(ctx context.Context, req *http.Request) (*Response, error) {
ctx, span := tracer.Start(ctx, fmt.Sprintf("%s SendRequest", req.Method))
defer span.End()
reqCtx, cancel := context.WithTimeout(ctx, c.timeout)
defer cancel()
res, err := c.httpClient.Do(req.WithContext(reqCtx))
if err != nil {
return nil, fmt.Errorf("failed to send request %v", err)
}
defer func() { _ = res.Body.Close() }()
body, err := io.ReadAll(res.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body %v", err)
}
return &Response{res, body}, nil
}
package rest
import (
"net/http"
"time"
)
type (
Option func(c *Client)
ClientOption Option
)
func WithCustomHTTPClient(client *http.Client) ClientOption {
return func(c *Client) {
c.httpClient = client
}
}
func WithDefaultHeaders() ClientOption {
return func(c *Client) {
if c.headers == nil {
c.headers = make(map[string]Header)
}
c.headers["Content-Type"] = Header{Value: "application/json", IsDefault: true}
c.headers["Accept"] = Header{Value: "application/json", IsDefault: true}
}
}
func WithTimeout(timeout time.Duration) ClientOption {
return func(c *Client) {
c.timeout = timeout
c.httpClient.Timeout = timeout
}
}
func WithHeader(key, value string) Option {
return func(c *Client) {
if c.headers == nil {
c.headers = make(map[string]Header)
}
c.headers[key] = Header{Value: value, IsDefault: false}
}
}
func WithQuery(key, value string) Option {
return func(c *Client) {
if c.query == nil {
c.query = make(map[string]string)
}
c.query[key] = value
}
}
func WithBody(body []byte) Option {
return func(c *Client) {
c.body = body
}
}
package rest
import (
"encoding/json"
"net/http"
)
type Response struct {
res *http.Response
body []byte
}
func (r *Response) Body() []byte {
return r.body
}
func (r *Response) Unmarshal(v any) error {
return json.Unmarshal(r.body, &v)
}
func (r *Response) Status() int {
return r.res.StatusCode
}
func (r *Response) Headers() http.Header {
return r.res.Header
}
func (r *Response) Cookies() []*http.Cookie {
return r.res.Cookies()
}
func (r *Response) OK() bool {
return r.res.StatusCode >= 200 && r.res.StatusCode <= 299
}
func (r *Response) Get() *http.Response {
return r.res
}
package rest_test
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"cooperative.bakode.xyz/internal/repository/rest"
"github.com/stretchr/testify/suite"
)
type TestClientSuite struct {
suite.Suite
ctx context.Context
}
type TestMethod struct {
name, baseURL string
method func(ctx context.Context, endpoint string, opts ...rest.Option) (*rest.Response, error)
options []rest.Option
}
func TestClient(t *testing.T) {
suite.Run(t, new(TestClientSuite))
}
func (s *TestClientSuite) SetupSuite() {
s.ctx = context.Background()
}
func (s *TestClientSuite) Test_New_ShouldRunSuccessfully() {
baseURL := "http://localhost:8080"
customClient := &http.Client{}
client := rest.New(baseURL, rest.WithCustomHTTPClient(customClient))
s.NotNil(client)
}
func (s *TestClientSuite) Test_Request_WhenRequestIsInvalid_ShouldReturnError() {
baseURL := "http://localhost:8080"
client := rest.New(baseURL)
requests := []TestMethod{
{
name: "GET",
baseURL: baseURL,
method: client.Get,
},
{
name: "POST",
baseURL: baseURL,
method: client.Post,
},
{
name: "PUT",
baseURL: baseURL,
method: client.Put,
},
{
name: "PATCH",
baseURL: baseURL,
method: client.Patch,
},
{
name: "DELETE",
baseURL: baseURL,
method: client.Delete,
},
}
for _, req := range requests {
s.Run(req.name, func() {
response, err := req.method(nil, "")
s.Nil(response)
s.Error(err)
})
}
}
func (s *TestClientSuite) Test_Request_WhenDoReturnsAnError_ShouldReturnError() {
baseURLWithInvalidSchema := "htt \\`"
client := rest.New(
baseURLWithInvalidSchema,
rest.WithTimeout(0),
rest.WithDefaultHeaders(),
)
requests := []TestMethod{
{
name: "GET",
baseURL: baseURLWithInvalidSchema,
method: client.Get,
},
{
name: "POST",
baseURL: baseURLWithInvalidSchema,
method: client.Post,
},
{
name: "PUT",
baseURL: baseURLWithInvalidSchema,
method: client.Put,
},
{
name: "PATCH",
baseURL: baseURLWithInvalidSchema,
method: client.Patch,
},
{
name: "DELETE",
baseURL: baseURLWithInvalidSchema,
method: client.Delete,
},
}
for _, req := range requests {
s.Run(req.name, func() {
response, err := req.method(s.ctx, "")
s.Nil(response)
s.Error(err)
})
}
}
func (s *TestClientSuite) Test_Request_WhenBodyReturnsError_ShouldReturnError() {
svc := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Length", "1")
_, _ = w.Write(nil)
}))
defer svc.Close()
client := rest.New(svc.URL)
requests := []TestMethod{
{
name: "GET",
baseURL: svc.URL,
method: client.Get,
},
{
name: "POST",
baseURL: svc.URL,
method: client.Post,
},
{
name: "PUT",
baseURL: svc.URL,
method: client.Put,
},
{
name: "PATCH",
baseURL: svc.URL,
method: client.Patch,
},
{
name: "DELETE",
baseURL: svc.URL,
method: client.Delete,
},
}
for _, req := range requests {
s.Run(req.name, func() {
response, err := req.method(s.ctx, "")
s.Nil(response)
s.Error(err)
})
}
}
func (s *TestClientSuite) Test_Request_ShouldRunSuccessfully() {
svc := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
}))
defer svc.Close()
client := rest.New(svc.URL)
requests := []TestMethod{
{
name: "GET",
baseURL: svc.URL,
method: client.Get,
},
{
name: "POST",
baseURL: svc.URL,
method: client.Post,
},
{
name: "PUT",
baseURL: svc.URL,
method: client.Put,
},
{
name: "PATCH",
baseURL: svc.URL,
method: client.Patch,
},
{
name: "DELETE",
baseURL: svc.URL,
method: client.Delete,
},
}
for _, req := range requests {
s.Run(req.name, func() {
response, err := req.method(s.ctx, "")
s.NotNil(response)
s.NoError(err)
})
}
}
func (s *TestClientSuite) Test_Request_WithOptions_ShouldRunSuccessfully() {
svc := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
}))
defer svc.Close()
client := rest.New(svc.URL)
requests := []TestMethod{
{
name: "GET",
baseURL: svc.URL,
method: client.Get,
options: []rest.Option{
rest.WithHeader("key", "value"),
rest.WithQuery("key", "value"),
},
},
{
name: "POST",
baseURL: svc.URL,
method: client.Post,
options: []rest.Option{
rest.WithHeader("key", "value"),
rest.WithQuery("key", "value"),
rest.WithBody([]byte("body")),
},
},
{
name: "PUT",
baseURL: svc.URL,
method: client.Put,
options: []rest.Option{rest.WithHeader("key", "value"), rest.WithQuery("key", "value")},
},
{
name: "PATCH",
baseURL: svc.URL,
method: client.Patch,
options: []rest.Option{rest.WithHeader("key", "value"), rest.WithQuery("key", "value")},
},
{
name: "DELETE",
baseURL: svc.URL,
method: client.Delete,
options: []rest.Option{rest.WithHeader("key", "value"), rest.WithQuery("key", "value")},
},
}
for _, req := range requests {
s.Run(req.name, func() {
response, err := req.method(s.ctx, "", req.options...)
s.NotNil(response)
s.NoError(err)
s.NotNil(response.Body())
s.Equal(response.Status(), http.StatusOK)
s.True(response.OK())
s.NotNil(response.Get())
s.NotNil(response.Cookies())
s.NotNil(response.Headers())
var data any
s.Error(response.Unmarshal(&data))
})
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment