{"id":50673718,"url":"https://github.com/jkaninda/kitoko","last_synced_at":"2026-06-08T14:01:37.625Z","repository":{"id":355664152,"uuid":"1170907913","full_name":"jkaninda/kitoko","owner":"jkaninda","description":"Kitoko - A lightweight, fluent HTTP client and testing library for Go","archived":false,"fork":false,"pushed_at":"2026-03-28T14:05:08.000Z","size":27,"stargazers_count":3,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-04T19:07:11.527Z","etag":null,"topics":["go-client","go-http","go-http-client"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jkaninda.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-02T16:54:50.000Z","updated_at":"2026-03-28T12:39:06.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/jkaninda/kitoko","commit_stats":null,"previous_names":["jkaninda/kitoko"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/jkaninda/kitoko","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jkaninda%2Fkitoko","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jkaninda%2Fkitoko/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jkaninda%2Fkitoko/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jkaninda%2Fkitoko/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jkaninda","download_url":"https://codeload.github.com/jkaninda/kitoko/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jkaninda%2Fkitoko/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34065354,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-08T02:00:07.615Z","response_time":111,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["go-client","go-http","go-http-client"],"created_at":"2026-06-08T14:01:36.299Z","updated_at":"2026-06-08T14:01:37.620Z","avatar_url":"https://github.com/jkaninda.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Kitoko\n\n**Kitoko** is a lightweight, fluent HTTP client and testing library for Go.\n\nIt is designed to be:\n\n* A **production-ready HTTP client** (microservices, external APIs, internal communication)\n* A powerful **HTTP testing library** with expressive, built-in assertions\n\nKitoko keeps your HTTP code clean, readable, and chainable — both in production and in tests.\n\n---\n\n## Installation\n\n```bash\ngo get github.com/jkaninda/kitoko\n```\n\n---\n\n# Quick Start\n\n## Production HTTP Client\n\n```go\nclient := kitoko.NewClient(\"https://api.example.com\")\nclient.Headers[\"Authorization\"] = \"Bearer my-token\"\n\nresp, err := client.GET(\"/users\").Execute()\nif err != nil {\n    log.Fatal(err)\n}\n\nfmt.Println(resp.StatusCode) // 200\nfmt.Println(resp.String())   // response body as string\n\nvar users []User\nif err := resp.JSON(\u0026users); err != nil {\n    log.Fatal(err)\n}\n```\n\n---\n\n## Testing HTTP Endpoints\n\n```go\nfunc TestAPI(t *testing.T) {\n    tc := kitoko.NewTestClient(t, server.URL)\n\n    tc.GET(\"/users\").\n        SetBearerAuth(\"my-token\").\n        ExpectStatusOK().\n        ExpectHeaderContains(\"Content-Type\", \"application/json\").\n        ExpectJSONPath(\"name\", \"Alice\")\n}\n```\n\n---\n\n# Features\n\n### Core\n\n* Dual-purpose: **Production client + Test library**\n* Fluent, chainable API\n* All HTTP methods supported:\n  `GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `HEAD`, `OPTIONS`\n* Per-request timeout (default: 30s)\n* Custom `http.Client` support\n\n### Request Capabilities\n\n* JSON body (struct, map, string)\n* Form-encoded body\n* Multipart form-data\n* File upload (disk or `io.Reader`)\n* Query parameters (single \u0026 batch)\n* Batch headers\n* Basic \u0026 Bearer authentication\n* Cookie support\n* Cookie jar (session persistence)\n\n### Testing \u0026 Assertions\n\n* Built-in status assertions\n* Body \u0026 header assertions\n* Cookie assertions\n* JSON assertions (full object or path-based)\n* Dot-notation JSON path (`user.name`)\n* `httptest.ResponseRecorder` integration\n* Wrap production requests with test assertions\n\n---\n\n# Production Client\n\n## Client with Base URL\n\n```go\nclient := kitoko.NewClient(\"https://api.example.com\")\nclient.Headers[\"Authorization\"] = \"Bearer token\"\n\n// GET\nresp, err := client.GET(\"/users\").Execute()\n\n// POST with JSON body\nresp, err := client.POST(\"/users\").\n    JSONBody(map[string]string{\"name\": \"Alice\"}).\n    Execute()\n\n// PUT with struct\nresp, err := client.PUT(\"/users/1\").\n    JSONBody(User{Name: \"Bob\", Age: 30}).\n    Execute()\n\n// DELETE\nresp, err := client.DELETE(\"/users/1\").Execute()\n```\n\n---\n\n## Client with Cookie Jar (Session-Based APIs)\n\n```go\nclient := kitoko.NewClientWithCookieJar(\"https://api.example.com\")\n\n// Login — server sets session cookie\n_, err := client.POST(\"/login\").\n    JSONBody(map[string]string{\"user\": \"admin\", \"pass\": \"secret\"}).\n    Execute()\n\n// Cookie automatically sent\nresp, err := client.GET(\"/profile\").Execute()\n```\n\n---\n\n## One-Off Requests\n\n```go\nresp, err := kitoko.NewRequest().\n    Method(\"GET\").\n    URL(\"https://api.example.com/health\").\n    Header(\"X-Request-ID\", \"abc123\").\n    Timeout(5 * time.Second).\n    Execute()\n```\n\n`Do()` is available as an alias for `Execute()`:\n\n```go\nresp, err := kitoko.NewRequest().\n    Method(\"GET\").\n    URL(\"https://api.example.com/health\").\n    Do()\n```\n\n---\n\n# Request Builder\n\nFull control over request construction:\n\n```go\nresp, err := kitoko.NewRequest().\n    Method(http.MethodPost).\n    URL(\"https://api.example.com\").\n    Path(\"/users\").\n    Header(\"X-Request-ID\", \"abc123\").\n    QueryParam(\"page\", \"1\").\n    JSONBody(`{\"name\":\"Alice\"}`).\n    SetBearerAuth(\"token\").\n    Timeout(5 * time.Second).\n    Execute()\n```\n\n---\n\n## Batch Headers \u0026 Query Parameters\n\n```go\nresp, err := client.GET(\"/search\").\n    Headers(map[string]string{\n        \"X-Request-ID\": \"abc123\",\n        \"Accept\":       \"application/json\",\n    }).\n    QueryParams(map[string]string{\n        \"q\":     \"kitoko\",\n        \"page\":  \"1\",\n        \"limit\": \"20\",\n    }).\n    Execute()\n```\n\n---\n\n## Form Data\n\n```go\nresp, err := client.POST(\"/login\").\n    FormBody(map[string]string{\n        \"username\": \"admin\",\n        \"password\": \"secret\",\n    }).\n    Execute()\n```\n\n---\n\n## File Uploads\n\n```go\n// Single file from disk\nresp, err := client.POST(\"/upload\").\n    FileUpload(\"avatar\", \"/path/to/photo.jpg\").\n    Execute()\n\n// Multiple files with form fields\nresp, err := client.POST(\"/upload\").\n    MultipartBody(\n        map[string]string{\"description\": \"My documents\"},\n        kitoko.FileField{\n            FieldName: \"doc\",\n            FileName:  \"report.pdf\",\n            Content:   file, // any io.Reader\n        },\n    ).\n    Execute()\n```\n\n---\n\n## Authentication\n\n```go\n// Basic Auth\nresp, err := client.GET(\"/secure\").\n    SetBasicAuth(\"admin\", \"password\").\n    Execute()\n\n// Bearer Token\nresp, err := client.GET(\"/secure\").\n    SetBearerAuth(\"eyJhbGci...\").\n    Execute()\n```\n\n---\n\n## Cookies\n\n```go\n// Single cookie\nresp, err := client.GET(\"/dashboard\").\n    SetCookie(\"token\", \"my-secret\").\n    Execute()\n\n// Multiple cookies\nresp, err := client.GET(\"/dashboard\").\n    SetCookies([]*http.Cookie{\n        {Name: \"session\", Value: \"abc\"},\n        {Name: \"theme\", Value: \"dark\"},\n    }).\n    Execute()\n```\n\n---\n\n## Package-Level Requests\n\n```go\nresp, err := kitoko.Get(\"https://api.example.com/data\").Do()\n\nresp, err := kitoko.Post(\"https://api.example.com/users\").\n    JSONBody(map[string]string{\"name\": \"Alice\"}).\n    Do()\n\nresp, err := kitoko.Put(\"https://api.example.com/users/1\").\n    JSONBody(map[string]string{\"name\": \"Bob\"}).\n    SetBearerAuth(\"my-token\").\n    Execute()\n\nresp, err := kitoko.Delete(\"https://api.example.com/users/1\").Execute()\n```\n\n---\n\n## Custom HTTP Client\n\n```go\nresp, err := kitoko.NewRequest().\n    Method(\"GET\").\n    URL(\"https://api.example.com/data\").\n    WithHTTPClient(\u0026http.Client{\n        Timeout: 10 * time.Second,\n        Transport: \u0026http.Transport{\n            MaxIdleConns: 10,\n        },\n    }).\n    Execute()\n```\n\n---\n\n# Testing\n\n## TestClient\n\n```go\nfunc TestAPI(t *testing.T) {\n    tc := kitoko.NewTestClient(t, server.URL)\n    tc.Headers[\"Authorization\"] = \"Bearer test-token\"\n\n    tc.GET(\"/api/users\").ExpectStatusOK()\n    tc.POST(\"/api/users\").JSONBody(user).ExpectStatusCreated()\n    tc.DELETE(\"/api/users/1\").ExpectStatusNoContent()\n}\n```\n\n---\n\n## TestClient with Cookie Jar\n\n```go\nfunc TestLoginFlow(t *testing.T) {\n    tc := kitoko.NewTestClientWithCookieJar(t, server.URL)\n\n    tc.POST(\"/login\").\n        JSONBody(map[string]string{\"user\": \"admin\", \"pass\": \"secret\"}).\n        ExpectStatusOK()\n\n    tc.GET(\"/profile\").\n        ExpectStatusOK().\n        ExpectJSONPath(\"user\", \"admin\")\n}\n```\n\n---\n\n## Package-Level Helpers\n\n```go\nkitoko.GET(t, server.URL+\"/health\").ExpectStatusOK()\n\nkitoko.POST(t, server.URL+\"/users\").\n    JSONBody(map[string]string{\"name\": \"Alice\"}).\n    ExpectStatusCreated().\n    ExpectJSONPath(\"name\", \"Alice\")\n```\n\nFull control:\n\n```go\nkitoko.Request(t).\n    Method(\"GET\").\n    URL(server.URL + \"/users\").\n    Header(\"Accept\", \"application/json\").\n    ExpectStatusOK()\n```\n\n---\n\n# Assertions\n\n## Status Codes\n\n```go\nrb.ExpectStatus(201)\nrb.ExpectStatusOK()\nrb.ExpectStatusCreated()\nrb.ExpectStatusAccepted()\nrb.ExpectStatusNoContent()\nrb.ExpectStatusBadRequest()\nrb.ExpectStatusUnauthorized()\nrb.ExpectStatusForbidden()\nrb.ExpectStatusNotFound()\nrb.ExpectStatusConflict()\nrb.ExpectStatusInternalServerError()\n```\n\n---\n\n## Body\n\n```go\nrb.ExpectBody(\"exact match\")\nrb.ExpectBodyContains(\"substring\")\nrb.ExpectContains(\"alias\")\nrb.ExpectBodyNotContains(\"unexpected\")\nrb.ExpectEmptyBody()\n```\n\n---\n\n## JSON\n\n```go\n// Full structure\nrb.ExpectJSON(map[string]any{\n    \"name\": \"Alice\",\n    \"age\":  float64(30),\n})\n\n// Path assertion (dot notation)\nrb.ExpectJSONPath(\"user.name\", \"Alice\")\nrb.ExpectJSONPath(\"config.timeout\", float64(30))\n\n// Parse into struct\nvar user User\nrb.ParseJSON(\u0026user)\n```\n\n---\n\n## Headers\n\n```go\nrb.ExpectHeader(\"Content-Type\", \"application/json\")\nrb.ExpectHeaderContains(\"Content-Type\", \"json\")\nrb.ExpectHeaderExists(\"X-Request-ID\")\nrb.ExpectContentType(\"application/json\")\n```\n\n---\n\n## Cookies\n\n```go\nrb.ExpectCookie(\"session\", \"abc123\")\nrb.ExpectCookieExist(\"session\")\n```\n\n---\n\n## Wrap Production Requests for Testing\n\n```go\nrb := kitoko.NewRequest().\n    Method(\"GET\").\n    URL(server.URL + \"/users\")\n\nkitoko.Expect(t, rb).ExpectStatusOK()\n```\n\n---\n\n## `httptest.ResponseRecorder` Integration\n\n```go\nrec := httptest.NewRecorder()\nhandler.ServeHTTP(rec, httptest.NewRequest(\"GET\", \"/api/users\", nil))\n\nkitoko.FromRecorder(t, rec).\n    ExpectStatusOK().\n    ExpectJSONPath(\"name\", \"Alice\")\n```\n\n---\n\n## Manual Execution in Tests\n\n```go\nresp, body := kitoko.GET(t, server.URL+\"/data\").Execute()\n\nfmt.Println(resp.StatusCode)\nfmt.Println(string(body))\n```\n\n---\n\n# Utilities\n\n## GracefulExitAfter\n\nSends a `SIGTERM` signal to the current process after a duration — useful for testing graceful shutdown behavior.\n\n```go\nkitoko.GracefulExitAfter(30 * time.Second)\n```\n\n---\n\n# Design Philosophy\n\nKitoko is built around three principles:\n\n1. **Fluent over verbose**\n2. **Readable tests**\n3. **Minimal abstraction over `net/http`**\n\nNo magic. No hidden behavior. Just expressive HTTP.\n\n---\n\n# License\n\nMIT License — see [LICENSE](LICENSE).\n\n\n# Copyright\nCopyright (c) 2026 Jonas Kaninda\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjkaninda%2Fkitoko","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjkaninda%2Fkitoko","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjkaninda%2Fkitoko/lists"}