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.
go get github.com/jkaninda/kitokoclient := 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)
}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")
}- 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.Clientsupport
- 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)
- Built-in status assertions
- Body & header assertions
- Cookie assertions
- JSON assertions (full object or path-based)
- Dot-notation JSON path (
user.name) httptest.ResponseRecorderintegration- Wrap production requests with test assertions
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 := 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()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()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()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()resp, err := client.POST("/login").
FormBody(map[string]string{
"username": "admin",
"password": "secret",
}).
Execute()// 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()// Basic Auth
resp, err := client.GET("/secure").
SetBasicAuth("admin", "password").
Execute()
// Bearer Token
resp, err := client.GET("/secure").
SetBearerAuth("eyJhbGci...").
Execute()// 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()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()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()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()
}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")
}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()rb.ExpectStatus(201)
rb.ExpectStatusOK()
rb.ExpectStatusCreated()
rb.ExpectStatusAccepted()
rb.ExpectStatusNoContent()
rb.ExpectStatusBadRequest()
rb.ExpectStatusUnauthorized()
rb.ExpectStatusForbidden()
rb.ExpectStatusNotFound()
rb.ExpectStatusConflict()
rb.ExpectStatusInternalServerError()rb.ExpectBody("exact match")
rb.ExpectBodyContains("substring")
rb.ExpectContains("alias")
rb.ExpectBodyNotContains("unexpected")
rb.ExpectEmptyBody()// 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)rb.ExpectHeader("Content-Type", "application/json")
rb.ExpectHeaderContains("Content-Type", "json")
rb.ExpectHeaderExists("X-Request-ID")
rb.ExpectContentType("application/json")rb.ExpectCookie("session", "abc123")
rb.ExpectCookieExist("session")rb := kitoko.NewRequest().
Method("GET").
URL(server.URL + "/users")
kitoko.Expect(t, rb).ExpectStatusOK()rec := httptest.NewRecorder()
handler.ServeHTTP(rec, httptest.NewRequest("GET", "/api/users", nil))
kitoko.FromRecorder(t, rec).
ExpectStatusOK().
ExpectJSONPath("name", "Alice")resp, body := kitoko.GET(t, server.URL+"/data").Execute()
fmt.Println(resp.StatusCode)
fmt.Println(string(body))Sends a SIGTERM signal to the current process after a duration — useful for testing graceful shutdown behavior.
kitoko.GracefulExitAfter(30 * time.Second)Kitoko is built around three principles:
- Fluent over verbose
- Readable tests
- Minimal abstraction over
net/http
No magic. No hidden behavior. Just expressive HTTP.
MIT License — see LICENSE.
Copyright (c) 2026 Jonas Kaninda