https://github.com/jkaninda/kitoko
Kitoko - A lightweight, fluent HTTP client and testing library for Go
https://github.com/jkaninda/kitoko
go-client go-http go-http-client
Last synced: 19 days ago
JSON representation
Kitoko - A lightweight, fluent HTTP client and testing library for Go
- Host: GitHub
- URL: https://github.com/jkaninda/kitoko
- Owner: jkaninda
- License: mit
- Created: 2026-03-02T16:54:50.000Z (4 months ago)
- Default Branch: main
- Last Pushed: 2026-03-28T14:05:08.000Z (3 months ago)
- Last Synced: 2026-05-04T19:07:11.527Z (about 2 months ago)
- Topics: go-client, go-http, go-http-client
- Language: Go
- Homepage:
- Size: 26.4 KB
- Stars: 3
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# 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
```bash
go get github.com/jkaninda/kitoko
```
---
# Quick Start
## Production HTTP Client
```go
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
```go
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
```go
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)
```go
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
```go
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()`:
```go
resp, err := kitoko.NewRequest().
Method("GET").
URL("https://api.example.com/health").
Do()
```
---
# Request Builder
Full control over request construction:
```go
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
```go
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
```go
resp, err := client.POST("/login").
FormBody(map[string]string{
"username": "admin",
"password": "secret",
}).
Execute()
```
---
## File Uploads
```go
// 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
```go
// Basic Auth
resp, err := client.GET("/secure").
SetBasicAuth("admin", "password").
Execute()
// Bearer Token
resp, err := client.GET("/secure").
SetBearerAuth("eyJhbGci...").
Execute()
```
---
## Cookies
```go
// 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
```go
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
```go
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
```go
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
```go
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
```go
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:
```go
kitoko.Request(t).
Method("GET").
URL(server.URL + "/users").
Header("Accept", "application/json").
ExpectStatusOK()
```
---
# Assertions
## Status Codes
```go
rb.ExpectStatus(201)
rb.ExpectStatusOK()
rb.ExpectStatusCreated()
rb.ExpectStatusAccepted()
rb.ExpectStatusNoContent()
rb.ExpectStatusBadRequest()
rb.ExpectStatusUnauthorized()
rb.ExpectStatusForbidden()
rb.ExpectStatusNotFound()
rb.ExpectStatusConflict()
rb.ExpectStatusInternalServerError()
```
---
## Body
```go
rb.ExpectBody("exact match")
rb.ExpectBodyContains("substring")
rb.ExpectContains("alias")
rb.ExpectBodyNotContains("unexpected")
rb.ExpectEmptyBody()
```
---
## JSON
```go
// 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
```go
rb.ExpectHeader("Content-Type", "application/json")
rb.ExpectHeaderContains("Content-Type", "json")
rb.ExpectHeaderExists("X-Request-ID")
rb.ExpectContentType("application/json")
```
---
## Cookies
```go
rb.ExpectCookie("session", "abc123")
rb.ExpectCookieExist("session")
```
---
## Wrap Production Requests for Testing
```go
rb := kitoko.NewRequest().
Method("GET").
URL(server.URL + "/users")
kitoko.Expect(t, rb).ExpectStatusOK()
```
---
## `httptest.ResponseRecorder` Integration
```go
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, httptest.NewRequest("GET", "/api/users", nil))
kitoko.FromRecorder(t, rec).
ExpectStatusOK().
ExpectJSONPath("name", "Alice")
```
---
## Manual Execution in Tests
```go
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.
```go
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](LICENSE).
# Copyright
Copyright (c) 2026 Jonas Kaninda