README
¶
apikit
A production-ready Go toolkit for building REST APIs. Zero mandatory dependencies. Works with any net/http compatible router.
Features
errors— Structured API errors witherrors.Is/errors.Assupport, error codes, and sentinel errorsrequest— Generic body binding (Bind[T]), query/path/header parsing, pagination, sorting, filteringresponse— Consistent JSON envelope, fluent builder, pagination helpers, SSE streaming, XML, JSONP, and moremiddleware— Request ID, logging, panic recovery, CORS, rate limiting, auth, security headers, timeouthttpclient— HTTP client with retries, exponential backoff, circuit breaker, andHTTPClientinterface for mockingrouter— Route grouping with.Get()/.Post()method helpers, prefix groups, and per-group middleware on top ofhttp.ServeMuxserver— Graceful shutdown wrapper with signal handling, lifecycle hooks, and TLS supporthealth— Health check endpoint builder with dependency checks, timeouts, and liveness/readiness probesconfig— Load configuration from env vars,.envfiles, and JSON files into typed structs with validationsqlbuilder— Fluent SQL query builder for PostgreSQL, MySQL, and SQLite with JOINs, CTEs, UNION, upsert, andrequestpackage integrationdbx— Generic row scanner fordatabase/sql— eliminates scan boilerplate, maps rows to structs viadbtags, integrates withsqlbuilderapitest— Fluent test helpers for recording and asserting HTTP handler responses
Install
go get github.com/KARTIKrocks/apikit
Requires Go 1.22+.
Quick Start
package main
import (
"context"
"log"
"net/http"
"time"
"github.com/KARTIKrocks/apikit/errors"
"github.com/KARTIKrocks/apikit/middleware"
"github.com/KARTIKrocks/apikit/request"
"github.com/KARTIKrocks/apikit/response"
"github.com/KARTIKrocks/apikit/router"
"github.com/KARTIKrocks/apikit/server"
)
type CreateUserReq struct {
Name string `json:"name" validate:"required,min=2"`
Email string `json:"email" validate:"required,email"`
}
func createUser(w http.ResponseWriter, r *http.Request) error {
req, err := request.Bind[CreateUserReq](r)
if err != nil {
return err // Automatically sends 400 with structured error
}
// Your business logic...
if req.Email == "taken@example.com" {
return errors.Conflict("Email already in use")
}
user := map[string]string{"id": "123", "name": req.Name}
response.Created(w, "User created", user)
return nil
}
func main() {
r := router.New()
r.Use(
middleware.RequestID(),
middleware.Recover(),
middleware.Timeout(30 * time.Second),
)
r.Post("/users", createUser)
// Graceful shutdown with signal handling (SIGINT/SIGTERM)
srv := server.New(r, server.WithAddr(":8080"))
srv.OnStart(func() error {
log.Println("server started...")
return nil
})
srv.OnShutdown(func(ctx context.Context) error {
log.Println("closing database...")
return nil // db.Close()
})
if err := srv.Start(); err != nil {
log.Fatal(err)
}
}
Packages
errors
Structured API errors that integrate with Go's standard errors package.
import "github.com/KARTIKrocks/apikit/errors"
// Create errors
err := errors.NotFound("User") // 404
err := errors.BadRequest("Invalid input") // 400
err := errors.Validation("Invalid", map[string]string{
"email": "is required",
"name": "too short",
}) // 422
// Wrap underlying errors
err := errors.Internal("Database failed").Wrap(dbErr)
// Add details
err := errors.Conflict("Duplicate").
WithField("email", "already exists").
WithDetail("existing_id", "abc123")
// Check errors anywhere in your code
if errors.Is(err, errors.ErrNotFound) { ... }
if errors.Is(err, errors.ErrValidation) { ... }
// Extract error details
var apiErr *errors.Error
if errors.As(err, &apiErr) {
log.Printf("Status: %d, Code: %s", apiErr.StatusCode, apiErr.Code)
}
// Custom error codes
errors.RegisterCode("SUBSCRIPTION_EXPIRED", 402)
err := errors.New("SUBSCRIPTION_EXPIRED", "Your subscription has expired")
request
Generic request binding and parameter parsing.
import "github.com/KARTIKrocks/apikit/request"
// --- Body binding ---
type CreatePostReq struct {
Title string `json:"title"`
Body string `json:"body"`
}
// Generic binding — auto-detects JSON, form, or multipart from Content-Type
post, err := request.Bind[CreatePostReq](r)
// Explicit JSON binding
post, err := request.BindJSON[CreatePostReq](r)
// --- HTML form binding ---
type ContactForm struct {
Name string `form:"name" validate:"required"`
Email string `form:"email" validate:"required,email"`
Message string `form:"message" validate:"required,min=10"`
}
// Explicit form binding (application/x-www-form-urlencoded)
form, err := request.BindForm[ContactForm](r)
// Multipart form binding (multipart/form-data) with file uploads
type UploadForm struct {
Title string `form:"title" validate:"required"`
}
meta, err := request.BindMultipart[UploadForm](r)
fh, err := request.FormFile(r, "avatar") // Single file
allFiles := request.FormFiles(r) // All uploaded files
// --- Path parameters (Go 1.22+ stdlib routing) ---
// Route: "GET /posts/{id}"
id := request.PathParam(r, "id")
id, err := request.PathParamInt(r, "id")
// --- Query parameters ---
q := request.QueryFrom(r)
search := q.String("search", "") // Default: ""
page, err := q.Int("page", 1) // Default: 1
active, err := q.Bool("active", true) // Accepts: true/false/1/0/yes/no
tags := q.StringSlice("tags") // ?tags=go,api → ["go", "api"]
limit, err := q.IntRange("limit", 20, 1, 100) // Clamped to [1, 100]
// --- Headers ---
token := request.BearerToken(r) // Extract Bearer token
ip := request.ClientIP(r) // Respects X-Forwarded-For
reqID := request.RequestID(r) // X-Request-ID or X-Trace-ID
// --- Pagination ---
pg, err := request.Paginate(r) // ?page=2&per_page=25
// pg.Page=2, pg.PerPage=25, pg.Offset=25
// Also detects ?page_size=25 and ?limit=25 automatically
// Navigation helpers (need total count from your DB)
pg.HasNext(total) // true if more pages exist
pg.HasPrevious() // true if not first page
pg.TotalPages(total) // total number of pages
pg.NextPage() // next page number
pg.PreviousPage() // previous page number (min 1)
// SQL helpers
pg.SQLClause() // "LIMIT 25 OFFSET 25"
pg.SQLClauseMySQL() // "LIMIT 25, 25"
cursor, err := request.PaginateCursor(r) // ?cursor=abc&limit=25
// --- Sorting ---
sorts, err := request.ParseSort(r, request.SortConfig{
AllowedFields: []string{"name", "created_at"},
})
// ?sort=name,-created_at → [{name, asc}, {created_at, desc}]
// --- Filtering ---
filters, err := request.ParseFilters(r, request.FilterConfig{
AllowedFields: []string{"status", "role"},
})
// ?filter[status]=active&filter[age][gte]=18
// --- Struct tag validation (automatic in Bind[T]) ---
type CreateUserReq struct {
Name string `json:"name" validate:"required,min=2,max=100"`
Email string `json:"email" validate:"required,email"`
Role string `json:"role" validate:"oneof=admin user mod"`
}
// Bind[T] automatically validates tags before returning.
// Supported: required, email, url, min, max, len, oneof, alpha,
// alphanum, numeric, uuid, contains, startswith, endswith
// --- Programmatic validation (for cross-field logic) ---
v := request.NewValidation()
v.RequireString("name", req.Name)
v.RequireEmail("email", req.Email)
v.RequireURL("website", req.Website)
v.UUID("id", req.ID)
v.MinLength("name", req.Name, 2)
v.OneOf("role", req.Role, []string{"admin", "user", "mod"})
v.MatchesPattern("code", req.Code, `^[A-Z]{3}-\d{4}$`, "must match format XXX-0000")
v.Custom("end_date", func() bool {
return req.EndDate.After(req.StartDate)
}, "must be after start_date")
if err := v.Error(); err != nil {
return err // Returns structured 422 with field errors
}
response
Consistent JSON responses with a standard envelope.
import "github.com/KARTIKrocks/apikit/response"
// --- Success responses ---
response.OK(w, "Success", data) // 200
response.Created(w, "Created", data) // 201
response.Accepted(w, "Accepted", nil) // 202
response.NoContent(w) // 204
// --- Error responses ---
response.BadRequest(w, "Invalid input")
response.Unauthorized(w, "Login required")
response.NotFound(w, "User not found")
response.ValidationError(w, map[string]string{"email": "required"})
// --- From errors package (recommended) ---
response.Err(w, errors.NotFound("User"))
response.Err(w, err) // Any error — *errors.Error gets proper status, others get 500
// --- Builder pattern ---
response.New().
Status(201).
Message("Created").
Data(user).
Header("X-Resource-ID", user.ID).
Pagination(1, 20, 150).
Send(w)
// --- Paginated ---
response.Paginated(w, users, response.NewPageMeta(page, perPage, total))
// NewPageMeta auto-computes TotalPages, HasNext, and HasPrevious
// Link header (RFC 5988) for API clients
response.SetLinkHeader(w, "https://api.example.com/users", page, perPage, total)
// Link: <...?page=2&per_page=20>; rel="next", <...?page=1&per_page=20>; rel="first", ...
response.CursorPaginated(w, events, response.CursorMeta{
NextCursor: "eyJpZCI6MTAwfQ==",
HasMore: true,
})
// --- Streaming (SSE) ---
response.StreamJSON(w, func(send func(event string, data any) error) error {
for msg := range messages {
send("update", msg)
}
return nil
})
// --- Other formats ---
response.XML(w, 200, xmlData) // XML with <?xml?> header
response.IndentedJSON(w, 200, data) // Pretty-printed JSON
response.PureJSON(w, 200, data) // No HTML escaping of <, >, &
response.JSONP(w, r, 200, data) // JSONP (reads ?callback=)
response.Reader(w, 200, "image/png", size, imgReader) // Stream from io.Reader
response.HTML(w, 200, "<h1>Hello</h1>") // HTML
response.Text(w, 200, "OK") // Plain text
response.Raw(w, 200, "application/csv", csvBytes) // Raw bytes
// --- Handler wrapper ---
// Converts func(w, r) error → http.HandlerFunc
mux.HandleFunc("GET /users/{id}", response.Handle(getUser))
Response envelope format:
{
"success": true,
"message": "User created",
"data": { "id": "123", "name": "Alice" },
"meta": {
"page": 1,
"per_page": 20,
"total": 150,
"total_pages": 8,
"has_next": true,
"has_previous": false
},
"timestamp": 1700000000
}
router
Route grouping and method helpers on top of http.ServeMux.
import "github.com/KARTIKrocks/apikit/router"
// Create a router (implements http.Handler)
r := router.New()
// Global middleware
r.Use(middleware.RequestID(), middleware.Logger(slog.Default()))
// Method helpers — handlers return error
r.Get("/health", func(w http.ResponseWriter, r *http.Request) error {
response.OK(w, "OK", nil)
return nil
})
// Standard http.HandlerFunc (no error return) — use GetFunc/PostFunc/etc.
r.GetFunc("/version", func(w http.ResponseWriter, r *http.Request) {
response.OK(w, "OK", map[string]string{"version": "1.0.0"})
})
// Route groups with prefix and per-group middleware
api := r.Group("/api/v1", authMiddleware)
api.Get("/users", listUsers)
api.Post("/users", createUser)
api.GetFunc("/users/{id}", getUser) // stdlib handler
// Nested groups accumulate prefix and middleware
admin := api.Group("/admin", adminOnly)
admin.Delete("/users/{id}", deleteUser)
// Registers "DELETE /api/v1/admin/users/{id}" with auth + adminOnly middleware
// Handle/HandleFunc for http.Handler (e.g. file servers)
api.Handle("GET /docs", http.FileServer(http.Dir("./docs")))
// Use with server package
srv := server.New(r, server.WithAddr(":8080"))
srv.Start()
Middleware is resolved at registration time. Use() only applies to routes registered after the call, matching chi/echo/gin behavior.
middleware
Production-ready middleware that works with any net/http router.
import "github.com/KARTIKrocks/apikit/middleware"
// Chain middleware (applied in order)
stack := middleware.Chain(
middleware.RequestID(),
middleware.Logger(slog.Default()),
middleware.Recover(),
middleware.SecureHeaders(),
middleware.CORS(middleware.DefaultCORSConfig()),
middleware.RateLimit(middleware.RateLimitConfig{Rate: 100, Window: time.Minute}),
middleware.Timeout(30 * time.Second),
middleware.BodyLimit(5 << 20), // 5 MB
)
handler := stack(mux)
// --- Authentication ---
auth := middleware.Auth(middleware.AuthConfig{
Authenticate: func(ctx context.Context, token string) (any, error) {
user, err := verifyJWT(token)
if err != nil {
return nil, errors.Unauthorized("Invalid token")
}
return user, nil
},
SkipPaths: map[string]bool{"/health": true, "/login": true},
})
// In handlers, retrieve the user:
user, ok := middleware.GetAuthUserAs[*User](r.Context())
// --- Get request ID anywhere ---
reqID := middleware.GetRequestID(r.Context())
// --- Custom rate limiter backend ---
type RedisLimiter struct { ... }
func (rl *RedisLimiter) Allow(key string) bool { ... }
middleware.RateLimit(middleware.RateLimitConfig{
Limiter: &RedisLimiter{},
})
httpclient
HTTP client with retries, exponential backoff, circuit breaker, and an interface for easy mocking.
import "github.com/KARTIKrocks/apikit/httpclient"
// Create a client with functional options
client := httpclient.New("https://api.example.com",
httpclient.WithTimeout(10 * time.Second),
httpclient.WithMaxRetries(3),
httpclient.WithRetryDelay(500 * time.Millisecond),
httpclient.WithMaxRetryDelay(5 * time.Second),
httpclient.WithLogger(slog.Default()),
)
// Basic requests
resp, err := client.Get(ctx, "/users")
resp, err := client.Post(ctx, "/users", map[string]string{"name": "Alice"})
resp, err := client.Put(ctx, "/users/1", updateBody)
resp, err := client.Patch(ctx, "/users/1", patchBody)
resp, err := client.Delete(ctx, "/users/1")
// Response helpers
var users []User
resp.JSON(&users)
fmt.Println(resp.String(), resp.StatusCode, resp.IsSuccess())
// Set default headers
client.SetBearerToken("my-token")
client.SetHeader("X-API-Key", "key")
// Fluent request builder
resp, err := client.Request().
Method("POST").
Path("/search").
Header("X-Custom", "value").
Param("q", "golang").
Body(searchReq).
Send(ctx)
// Enable circuit breaker (opens after 5 failures, resets after 30s)
client := httpclient.New("https://api.example.com",
httpclient.WithCircuitBreaker(5, 30 * time.Second),
)
// --- Mocking in tests ---
// HTTPClient interface allows easy substitution
func fetchUsers(client httpclient.HTTPClient) ([]User, error) { ... }
// In tests:
mock := httpclient.NewMockClient()
mock.OnGet("/users", 200, []byte(`[{"id":1,"name":"Alice"}]`))
mock.OnPost("/users", 201, []byte(`{"id":2}`))
mock.OnError("GET", "/fail", fmt.Errorf("connection refused"))
users, err := fetchUsers(mock)
fmt.Println(mock.GetCallCount()) // 1
server
Production-ready HTTP server with graceful shutdown, signal handling, and lifecycle hooks.
import "github.com/KARTIKrocks/apikit/server"
srv := server.New(handler,
server.WithAddr(":8080"),
server.WithReadTimeout(15 * time.Second),
server.WithWriteTimeout(60 * time.Second),
server.WithIdleTimeout(120 * time.Second),
server.WithShutdownTimeout(10 * time.Second),
server.WithLogger(slog.Default()),
)
// HTTPS — just add WithTLS
srv := server.New(handler,
server.WithAddr(":443"),
server.WithTLS("cert.pem", "key.pem"),
)
// Lifecycle hooks
srv.OnStart(func() error {
slog.Info("connecting to database...")
return db.Connect()
})
srv.OnShutdown(func(ctx context.Context) error {
return db.Close()
})
// Blocks until SIGINT/SIGTERM, then drains connections gracefully
if err := srv.Start(); err != nil {
log.Fatal(err)
}
health
Health check endpoints for Kubernetes probes and load balancers.
import "github.com/KARTIKrocks/apikit/health"
// Create a checker with a per-check timeout
h := health.NewChecker(health.WithTimeout(3 * time.Second))
// Critical checks — failure → "unhealthy" (503)
h.AddCheck("postgres", func(ctx context.Context) error {
return db.PingContext(ctx)
})
// Non-critical checks — failure → "degraded" (200)
h.AddNonCriticalCheck("redis", func(ctx context.Context) error {
return rdb.Ping(ctx).Err()
})
// Register with your router
r.Get("/health", h.Handler()) // Full check (readiness)
r.Get("/health/live", h.LiveHandler()) // Always 200 (liveness)
// Programmatic use
resp := h.Check(context.Background())
fmt.Println(resp.Status) // "healthy", "degraded", or "unhealthy"
Response format:
{
"success": true,
"message": "Health check",
"data": {
"status": "healthy",
"checks": {
"postgres": { "status": "healthy", "duration_ms": 2 },
"redis": { "status": "healthy", "duration_ms": 1 }
},
"timestamp": 1700000000
},
"timestamp": 1700000000
}
config
Load application configuration from environment variables, .env files, and JSON config files into typed Go structs.
import "github.com/KARTIKrocks/apikit/config"
type AppConfig struct {
Host string `env:"HOST" default:"localhost" validate:"required"`
Port int `env:"PORT" default:"8080" validate:"required,min=1,max=65535"`
Debug bool `env:"DEBUG" default:"false"`
DBUrl string `env:"DB_URL" validate:"required,url"`
LogLevel string `env:"LOG_LEVEL" default:"info" validate:"oneof=debug info warn error"`
Timeout time.Duration `env:"TIMEOUT" default:"30s"`
Tags []string `env:"TAGS"`
}
var cfg AppConfig
config.MustLoad(&cfg,
config.WithPrefix("APP"), // reads APP_HOST, APP_PORT, etc.
config.WithEnvFile(".env"), // load .env file (won't override real env vars)
config.WithJSONFile("config.json"), // JSON as base layer
)
// Sources are applied in priority order:
// 1. Environment variables (highest)
// 2. .env file
// 3. JSON file
// 4. default:"..." tags (lowest)
Nested structs are flattened automatically:
type Config struct {
DB struct {
Host string `env:"HOST" default:"localhost"`
Port int `env:"PORT" default:"5432"`
}
}
// Reads DB_HOST, DB_PORT (or APP_DB_HOST, APP_DB_PORT with WithPrefix("APP"))
sqlbuilder
Fluent SQL query builder for PostgreSQL, MySQL, and SQLite. Produces (string, []any) pairs — no database/sql dependency.
import "github.com/KARTIKrocks/apikit/sqlbuilder"
// --- SELECT ---
sql, args := sqlbuilder.Select("id", "name", "email").
From("users").
Where("active = $1", true).
OrderBy("name ASC").
Limit(20).
Build()
// sql: "SELECT id, name, email FROM users WHERE active = $1 ORDER BY name ASC LIMIT 20"
// args: [true]
// Convenience Where helpers — no placeholder syntax needed
sql, args := sqlbuilder.Select("id").From("users").
WhereEq("status", "active").
WhereGt("age", 18).
WhereLike("name", "A%").
Build()
// sql: "SELECT id FROM users WHERE status = $1 AND age > $2 AND name LIKE $3"
// args: ["active", 18, "A%"]
// Also: WhereNeq, WhereGte, WhereLt, WhereLte, WhereILike
// OrderBy helpers
sql, _ = sqlbuilder.Select("id").From("users").
OrderByAsc("name").
OrderByDesc("created_at").
Build()
// sql: "SELECT id FROM users ORDER BY name ASC, created_at DESC"
// Placeholder rebasing — each Where uses $1-relative numbering
sql, args = sqlbuilder.Select("id").From("users").
Where("status = $1", "active").
Where("age > $1", 18).
Build()
// sql: "SELECT id FROM users WHERE status = $1 AND age > $2"
// args: ["active", 18]
// JOINs, GROUP BY, HAVING
sql, args := sqlbuilder.Select("u.id", "COUNT(o.id) as orders").
From("users u").
LeftJoin("orders o", "o.user_id = u.id").
Where("u.active = $1", true).
GroupBy("u.id").
Having("COUNT(o.id) > $1", 5).
Build()
// Aggregate helpers
sql, _ = sqlbuilder.SelectExpr(
sqlbuilder.Count("*").As("total"),
sqlbuilder.Avg("price").As("avg_price"),
).From("products").Build()
// sql: "SELECT COUNT(*) AS total, AVG(price) AS avg_price FROM products"
// Subquery conditions
sub := sqlbuilder.Select("user_id").From("orders").Where("total > $1", 100)
sql, args = sqlbuilder.Select("id", "name").From("users").
WhereInSubquery("id", sub).
Build()
// sql: "SELECT id, name FROM users WHERE id IN (SELECT user_id FROM orders WHERE total > $1)"
// args: [100]
// Subqueries, CTEs, UNION, FOR UPDATE
sql, _ := sqlbuilder.Select("id").From("users").ForUpdate().SkipLocked().Build()
q1 := sqlbuilder.Select("id").From("users")
q2 := sqlbuilder.Select("id").From("admins")
sql, _ = q1.Union(q2).Build()
// Conditional building — only applies the clause when the condition is true
sql, args = sqlbuilder.Select("id").From("users").
When(onlyActive, func(s *sqlbuilder.SelectBuilder) {
s.Where("active = $1", true)
}).Build()
// Clone for safe reuse — mutations to the clone don't affect the original
base := sqlbuilder.Select("id", "name").From("users").Where("active = $1", true)
adminQuery := base.Clone().Where("role = $1", "admin")
userQuery := base.Clone().Where("role = $1", "user")
// --- INSERT ---
sql, args := sqlbuilder.Insert("users").
Columns("name", "email").
Values("Alice", "alice@example.com").
Returning("id").
Build()
// Batch insert
sql, args := sqlbuilder.Insert("users").
Columns("name", "email").
Values("Alice", "alice@example.com").
Values("Bob", "bob@example.com").
Build()
// Upsert (ON CONFLICT)
sql, args := sqlbuilder.Insert("users").
Columns("email", "name").
Values("alice@example.com", "Alice").
OnConflictUpdate([]string{"email"}, map[string]any{"name": "Alice Updated"}).
Build()
// --- UPDATE ---
sql, args := sqlbuilder.Update("users").
Set("name", "Bob").
SetExpr("updated_at", sqlbuilder.Raw("NOW()")).
WhereEq("id", 1).
Build()
// Increment / Decrement
sql, args = sqlbuilder.Update("products").
Increment("view_count", 1).
WhereEq("id", 42).
Build()
// sql: "UPDATE products SET view_count = view_count + $1 WHERE id = $2"
// args: [1, 42]
// --- DELETE ---
sql, args := sqlbuilder.Delete("users").
WhereEq("id", 1).
Returning("id", "name").
Build()
// --- MySQL / SQLite dialect ---
sql, args = sqlbuilder.Select("id", "name").
From("users").
WhereEq("active", true).
WhereGt("age", 18).
SetDialect(sqlbuilder.MySQL). // or sqlbuilder.SQLite
Build()
// sql: "SELECT id, name FROM users WHERE active = ? AND age > ?"
// args: [true, 18]
// Dialect-first constructors
sql, args = sqlbuilder.SelectWith(sqlbuilder.MySQL, "id").
From("users").WhereEq("id", 1).Build()
// sql: "SELECT id FROM users WHERE id = ?"
sql, args = sqlbuilder.InsertWith(sqlbuilder.MySQL, "users").
Columns("name", "email").
Values("Alice", "alice@example.com").
Build()
// sql: "INSERT INTO users (name, email) VALUES (?, ?)"
// --- Integration with request package ---
pg, _ := request.Paginate(r)
sorts, _ := request.ParseSort(r, sortCfg)
filters, _ := request.ParseFilters(r, filterCfg)
cols := map[string]string{"name": "u.name", "created_at": "u.created_at"}
sql, args := sqlbuilder.Select("u.id", "u.name", "u.email").
From("users u").
LeftJoin("profiles p", "p.user_id = u.id").
Where("u.active = $1", true).
ApplyFilters(filters, cols).
ApplySort(sorts, cols).
ApplyPagination(pg).
Build()
dbx
Lightweight, generic row scanner for database/sql. Eliminates scan boilerplate while keeping full SQL control.
import "github.com/KARTIKrocks/apikit/dbx"
// Set default connection once at startup
dbx.SetDefault(db)
// Define your struct with db tags
type User struct {
ID int `db:"id"`
Name string `db:"name"`
Email *string `db:"email"` // nullable → pointer
}
// --- Fetch rows ---
users, err := dbx.QueryAll[User](ctx, "SELECT id, name, email FROM users WHERE active = $1", true)
// --- Fetch one row (returns errors.CodeNotFound if no rows) ---
user, err := dbx.QueryOne[User](ctx, "SELECT id, name, email FROM users WHERE id = $1", 42)
// --- Execute statements ---
result, err := dbx.Exec(ctx, "DELETE FROM users WHERE id = $1", 42)
// --- sqlbuilder integration ---
q := sqlbuilder.Select("id", "name", "email").
From("users").
WhereEq("active", true).
Build()
users, err := dbx.QueryAllQ[User](ctx, q)
// --- Transactions (context-based) ---
tx, _ := db.BeginTx(ctx, nil)
ctx = dbx.WithTx(ctx, tx)
dbx.Exec(ctx, "INSERT INTO users (name) VALUES ($1)", "Alice") // uses tx
user, err := dbx.QueryOne[User](ctx, "SELECT id, name FROM users WHERE name = $1", "Alice") // uses tx
tx.Commit()
Column matching is order-independent. Unmatched result columns are silently discarded. Embedded structs (including pointer embeds) are supported. Type mappings are cached per-type via sync.Map.
apitest
Fluent test helpers for building requests and asserting responses against your handlers.
import "github.com/KARTIKrocks/apikit/apitest"
// Build a request
req := apitest.NewRequest("POST", "/users").
WithBody(map[string]string{"name": "Alice"}).
WithBearerToken("valid-token").
WithHeader("X-Request-ID", "test-123").
WithQuery("notify", "true").
WithPathValue("id", "42").
Build()
// Record handler response
resp := apitest.RecordHandler(createUser, req)
// Fluent assertions
resp.AssertStatus(t, 201)
resp.AssertSuccess(t)
resp.AssertHeader(t, "X-Request-ID", "test-123")
resp.AssertBodyContains(t, "Alice")
resp.AssertError(t, "NOT_FOUND")
resp.AssertValidationError(t, "email")
// Decode response
var user User
resp.Decode(&user)
// Access envelope directly
env, _ := resp.Envelope()
fmt.Println(env.Success, env.Message)
Design Principles
| Principle | How |
|---|---|
| stdlib compatible | Works with http.Handler, http.HandlerFunc, any router |
| Zero dependencies | Core uses only the Go standard library |
| Generics | Bind[T], GetAuthUserAs[T] for type safety |
| Interface-driven | RateLimiter, Logger interfaces — plug in your own backends |
| Composable | Each package is independently usable |
| Go 1.22+ | Leverages enhanced http.ServeMux routing |
Roadmap
-
health— Health check endpoint builder with dependency checks -
sqlbuilder— Fluent SQL query builder with request package integration -
dbx— Generic row scanner fordatabase/sqlwithsqlbuilderintegration -
ctxutil— Typed context helpers -
observe— OpenTelemetry integration
License
Directories
¶
| Path | Synopsis |
|---|---|
|
Package apitest provides testing helpers for HTTP handlers built with apikit.
|
Package apitest provides testing helpers for HTTP handlers built with apikit. |
|
Package config loads application configuration from environment variables, .env files, and JSON config files into typed Go structs.
|
Package config loads application configuration from environment variables, .env files, and JSON config files into typed Go structs. |
|
Package dbx provides lightweight, generic helpers for scanning database/sql rows into Go structs.
|
Package dbx provides lightweight, generic helpers for scanning database/sql rows into Go structs. |
|
Package errors provides structured API error types that integrate with Go's standard errors package (errors.Is, errors.As, error wrapping).
|
Package errors provides structured API error types that integrate with Go's standard errors package (errors.Is, errors.As, error wrapping). |
|
examples
|
|
|
complete
command
|
|
|
Package health provides health check endpoint builders with dependency checking, timeouts, and standard response formats.
|
Package health provides health check endpoint builders with dependency checking, timeouts, and standard response formats. |
|
Package httpclient provides a production-ready HTTP client with automatic retries, exponential backoff, circuit breaker, and structured logging via slog.
|
Package httpclient provides a production-ready HTTP client with automatic retries, exponential backoff, circuit breaker, and structured logging via slog. |
|
Package middleware provides production-ready HTTP middleware that works with any net/http compatible router.
|
Package middleware provides production-ready HTTP middleware that works with any net/http compatible router. |
|
Package request provides helpers for parsing, binding, and validating HTTP request data.
|
Package request provides helpers for parsing, binding, and validating HTTP request data. |
|
Package response provides structured JSON response helpers with a consistent envelope format and generics support.
|
Package response provides structured JSON response helpers with a consistent envelope format and generics support. |
|
Package router provides route grouping and method helpers on top of http.ServeMux.
|
Package router provides route grouping and method helpers on top of http.ServeMux. |
|
Package server provides a production-ready HTTP server with graceful shutdown, signal handling, and lifecycle hooks.
|
Package server provides a production-ready HTTP server with graceful shutdown, signal handling, and lifecycle hooks. |
|
Package sqlbuilder provides a fluent, type-safe SQL query builder for PostgreSQL, MySQL, and SQLite.
|
Package sqlbuilder provides a fluent, type-safe SQL query builder for PostgreSQL, MySQL, and SQLite. |