Skip to content

jkaninda/kitoko

Repository files navigation

Kitoko

Kitoko is a lightweight, fluent HTTP client and testing library for Go.

It is designed to be:

  • A production-ready HTTP client (microservices, external APIs, internal communication)
  • A powerful HTTP testing library with expressive, built-in assertions

Kitoko keeps your HTTP code clean, readable, and chainable — both in production and in tests.


Installation

go get github.com/jkaninda/kitoko

Quick Start

Production HTTP Client

client := kitoko.NewClient("https://api.example.com")
client.Headers["Authorization"] = "Bearer my-token"

resp, err := client.GET("/users").Execute()
if err != nil {
    log.Fatal(err)
}

fmt.Println(resp.StatusCode) // 200
fmt.Println(resp.String())   // response body as string

var users []User
if err := resp.JSON(&users); err != nil {
    log.Fatal(err)
}

Testing HTTP Endpoints

func TestAPI(t *testing.T) {
    tc := kitoko.NewTestClient(t, server.URL)

    tc.GET("/users").
        SetBearerAuth("my-token").
        ExpectStatusOK().
        ExpectHeaderContains("Content-Type", "application/json").
        ExpectJSONPath("name", "Alice")
}

Features

Core

  • Dual-purpose: Production client + Test library
  • Fluent, chainable API
  • All HTTP methods supported: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS
  • Per-request timeout (default: 30s)
  • Custom http.Client support

Request Capabilities

  • JSON body (struct, map, string)
  • Form-encoded body
  • Multipart form-data
  • File upload (disk or io.Reader)
  • Query parameters (single & batch)
  • Batch headers
  • Basic & Bearer authentication
  • Cookie support
  • Cookie jar (session persistence)

Testing & Assertions

  • Built-in status assertions
  • Body & header assertions
  • Cookie assertions
  • JSON assertions (full object or path-based)
  • Dot-notation JSON path (user.name)
  • httptest.ResponseRecorder integration
  • Wrap production requests with test assertions

Production Client

Client with Base URL

client := kitoko.NewClient("https://api.example.com")
client.Headers["Authorization"] = "Bearer token"

// GET
resp, err := client.GET("/users").Execute()

// POST with JSON body
resp, err := client.POST("/users").
    JSONBody(map[string]string{"name": "Alice"}).
    Execute()

// PUT with struct
resp, err := client.PUT("/users/1").
    JSONBody(User{Name: "Bob", Age: 30}).
    Execute()

// DELETE
resp, err := client.DELETE("/users/1").Execute()

Client with Cookie Jar (Session-Based APIs)

client := kitoko.NewClientWithCookieJar("https://api.example.com")

// Login — server sets session cookie
_, err := client.POST("/login").
    JSONBody(map[string]string{"user": "admin", "pass": "secret"}).
    Execute()

// Cookie automatically sent
resp, err := client.GET("/profile").Execute()

One-Off Requests

resp, err := kitoko.NewRequest().
    Method("GET").
    URL("https://api.example.com/health").
    Header("X-Request-ID", "abc123").
    Timeout(5 * time.Second).
    Execute()

Do() is available as an alias for Execute():

resp, err := kitoko.NewRequest().
    Method("GET").
    URL("https://api.example.com/health").
    Do()

Request Builder

Full control over request construction:

resp, err := kitoko.NewRequest().
    Method(http.MethodPost).
    URL("https://api.example.com").
    Path("/users").
    Header("X-Request-ID", "abc123").
    QueryParam("page", "1").
    JSONBody(`{"name":"Alice"}`).
    SetBearerAuth("token").
    Timeout(5 * time.Second).
    Execute()

Batch Headers & Query Parameters

resp, err := client.GET("/search").
    Headers(map[string]string{
        "X-Request-ID": "abc123",
        "Accept":       "application/json",
    }).
    QueryParams(map[string]string{
        "q":     "kitoko",
        "page":  "1",
        "limit": "20",
    }).
    Execute()

Form Data

resp, err := client.POST("/login").
    FormBody(map[string]string{
        "username": "admin",
        "password": "secret",
    }).
    Execute()

File Uploads

// Single file from disk
resp, err := client.POST("/upload").
    FileUpload("avatar", "/path/to/photo.jpg").
    Execute()

// Multiple files with form fields
resp, err := client.POST("/upload").
    MultipartBody(
        map[string]string{"description": "My documents"},
        kitoko.FileField{
            FieldName: "doc",
            FileName:  "report.pdf",
            Content:   file, // any io.Reader
        },
    ).
    Execute()

Authentication

// Basic Auth
resp, err := client.GET("/secure").
    SetBasicAuth("admin", "password").
    Execute()

// Bearer Token
resp, err := client.GET("/secure").
    SetBearerAuth("eyJhbGci...").
    Execute()

Cookies

// Single cookie
resp, err := client.GET("/dashboard").
    SetCookie("token", "my-secret").
    Execute()

// Multiple cookies
resp, err := client.GET("/dashboard").
    SetCookies([]*http.Cookie{
        {Name: "session", Value: "abc"},
        {Name: "theme", Value: "dark"},
    }).
    Execute()

Package-Level Requests

resp, err := kitoko.Get("https://api.example.com/data").Do()

resp, err := kitoko.Post("https://api.example.com/users").
    JSONBody(map[string]string{"name": "Alice"}).
    Do()

resp, err := kitoko.Put("https://api.example.com/users/1").
    JSONBody(map[string]string{"name": "Bob"}).
    SetBearerAuth("my-token").
    Execute()

resp, err := kitoko.Delete("https://api.example.com/users/1").Execute()

Custom HTTP Client

resp, err := kitoko.NewRequest().
    Method("GET").
    URL("https://api.example.com/data").
    WithHTTPClient(&http.Client{
        Timeout: 10 * time.Second,
        Transport: &http.Transport{
            MaxIdleConns: 10,
        },
    }).
    Execute()

Testing

TestClient

func TestAPI(t *testing.T) {
    tc := kitoko.NewTestClient(t, server.URL)
    tc.Headers["Authorization"] = "Bearer test-token"

    tc.GET("/api/users").ExpectStatusOK()
    tc.POST("/api/users").JSONBody(user).ExpectStatusCreated()
    tc.DELETE("/api/users/1").ExpectStatusNoContent()
}

TestClient with Cookie Jar

func TestLoginFlow(t *testing.T) {
    tc := kitoko.NewTestClientWithCookieJar(t, server.URL)

    tc.POST("/login").
        JSONBody(map[string]string{"user": "admin", "pass": "secret"}).
        ExpectStatusOK()

    tc.GET("/profile").
        ExpectStatusOK().
        ExpectJSONPath("user", "admin")
}

Package-Level Helpers

kitoko.GET(t, server.URL+"/health").ExpectStatusOK()

kitoko.POST(t, server.URL+"/users").
    JSONBody(map[string]string{"name": "Alice"}).
    ExpectStatusCreated().
    ExpectJSONPath("name", "Alice")

Full control:

kitoko.Request(t).
    Method("GET").
    URL(server.URL + "/users").
    Header("Accept", "application/json").
    ExpectStatusOK()

Assertions

Status Codes

rb.ExpectStatus(201)
rb.ExpectStatusOK()
rb.ExpectStatusCreated()
rb.ExpectStatusAccepted()
rb.ExpectStatusNoContent()
rb.ExpectStatusBadRequest()
rb.ExpectStatusUnauthorized()
rb.ExpectStatusForbidden()
rb.ExpectStatusNotFound()
rb.ExpectStatusConflict()
rb.ExpectStatusInternalServerError()

Body

rb.ExpectBody("exact match")
rb.ExpectBodyContains("substring")
rb.ExpectContains("alias")
rb.ExpectBodyNotContains("unexpected")
rb.ExpectEmptyBody()

JSON

// Full structure
rb.ExpectJSON(map[string]any{
    "name": "Alice",
    "age":  float64(30),
})

// Path assertion (dot notation)
rb.ExpectJSONPath("user.name", "Alice")
rb.ExpectJSONPath("config.timeout", float64(30))

// Parse into struct
var user User
rb.ParseJSON(&user)

Headers

rb.ExpectHeader("Content-Type", "application/json")
rb.ExpectHeaderContains("Content-Type", "json")
rb.ExpectHeaderExists("X-Request-ID")
rb.ExpectContentType("application/json")

Cookies

rb.ExpectCookie("session", "abc123")
rb.ExpectCookieExist("session")

Wrap Production Requests for Testing

rb := kitoko.NewRequest().
    Method("GET").
    URL(server.URL + "/users")

kitoko.Expect(t, rb).ExpectStatusOK()

httptest.ResponseRecorder Integration

rec := httptest.NewRecorder()
handler.ServeHTTP(rec, httptest.NewRequest("GET", "/api/users", nil))

kitoko.FromRecorder(t, rec).
    ExpectStatusOK().
    ExpectJSONPath("name", "Alice")

Manual Execution in Tests

resp, body := kitoko.GET(t, server.URL+"/data").Execute()

fmt.Println(resp.StatusCode)
fmt.Println(string(body))

Utilities

GracefulExitAfter

Sends a SIGTERM signal to the current process after a duration — useful for testing graceful shutdown behavior.

kitoko.GracefulExitAfter(30 * time.Second)

Design Philosophy

Kitoko is built around three principles:

  1. Fluent over verbose
  2. Readable tests
  3. Minimal abstraction over net/http

No magic. No hidden behavior. Just expressive HTTP.


License

MIT License — see LICENSE.

Copyright

Copyright (c) 2026 Jonas Kaninda

About

Kitoko - A lightweight, fluent HTTP client and testing library for Go

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages