Files
go-kite/README.md
Timo Riegebauer dd92f23e0b feat: initial implementation of go-kite
Core framework:
- Kite router with full HTTP method support (GET, POST, PUT, DELETE, HEAD, OPTIONS, CONNECT, TRACE)
- Nestable route groups with prefix and middleware inheritance
- Global, group, and route-level middleware with onion ordering
- Centralized error, not-found, and method-not-allowed handlers
- Graceful shutdown on SIGTERM/SIGINT with configurable timeout
- Configurable server read, write, and idle timeouts
- Context with typed request/response helpers, value store, cookie, form, and body binding support
- Response writer wrapper for status code tracking

Middleware:
- Logger with configurable output, format, and skip function
- Recovery with configurable panic handler
- RequestID with configurable header and generator, forwards incoming IDs
- CORS with configurable origins, methods, headers, credentials, and max age
- MaxBodySize with configurable byte limit

Docs:
- README with quickstart, routing, middleware, context API reference, and TLS guide
2026-04-23 20:25:13 +02:00

11 KiB

go-kite

A fast, lightweight, and expressive HTTP framework for Go, built on top of httprouter.

Features

  • Middleware — global, group, and route-level middleware with correct onion ordering
  • Groups — nestable route groups with prefix and middleware inheritance
  • Graceful shutdown — in-flight requests finish cleanly on SIGTERM / SIGINT
  • Configurable — server timeouts, error handlers, not-found handlers all configurable
  • Rich context — typed request/response helpers, value store, cookie and form support
  • Built-in middleware — logger, recovery, CORS, request ID, max body size

Installation

go get git.trcreatives.at/trcreatives/go-kite

Quickstart

package main

import (
    "net/http"
    "log"

    "git.trcreatives.at/trcreatives/go-kite"
    "git.trcreatives.at/trcreatives/go-kite/middleware"
)

func main() {
    k := kite.New()

    k.Use(
        middleware.Recovery(),
        middleware.Logger(),
        middleware.RequestID(),
    )

    k.GET("/", func(ctx *kite.Context) error {
        return ctx.WriteJSON(http.StatusOK, map[string]string{
            "message": "Hello, World!",
        })
    })

    if err := k.Start("localhost:8080"); err != nil {
        log.Fatal(err)
    }
}

Routing

k := kite.New()

k.GET("/users", listUsers)
k.POST("/users", createUser)
k.GET("/users/:id", getUser)
k.PUT("/users/:id", updateUser)
k.DELETE("/users/:id", deleteUser)

All standard HTTP methods are supported: GET, POST, PUT, DELETE, HEAD, OPTIONS, CONNECT, TRACE, PATCH.

Route Groups

Groups allow you to share a common prefix and middleware across multiple routes.

api := k.Group("/api")
api.GET("/users", listUsers)   // GET /api/users
api.POST("/users", createUser) // POST /api/users

Groups can be nested:

api := k.Group("/api")
v1  := api.Group("/v1")
v1.GET("/users", listUsers) // GET /api/v1/users

Middleware can be attached to groups:

api := k.Group("/api", authMiddleware)
admin := api.Group("/admin", adminOnlyMiddleware)
admin.GET("/stats", getStats) // chain: global → auth → adminOnly → handler

Middleware

Middleware follows the standard onion model — each middleware wraps the next.

type Middleware func(h Handler) Handler

Global Middleware

Applied to every route:

k.Use(middleware.Recovery())
k.Use(middleware.Logger())

Route Middleware

Applied to a single route:

k.GET("/admin", adminHandler, authMiddleware, rateLimitMiddleware)

Execution Order

→ Global 1
  → Global 2
    → Group 1
      → Route 1
        [Handler]
      ← Route 1
    ← Group 1
  ← Global 2
← Global 1

Writing Custom Middleware

func MyMiddleware(h kite.Handler) kite.Handler {
    return func(ctx *kite.Context) error {
        // before handler
        err := h(ctx)
        // after handler
        return err
    }
}

Built-in Middleware

All middleware lives in the middleware sub-package:

import "git.trcreatives.at/trcreatives/go-kite/middleware"

Logger

Logs method, path, status code and latency for every request.

// simple
k.Use(middleware.Logger())

// custom
k.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
    Output: logFile,
    Format: func(method, path string, statusCode int, latency time.Duration) string {
        return fmt.Sprintf("%s %s %d %dms", method, path, statusCode, latency.Milliseconds())
    },
    Skip: func(ctx *kite.Context) bool {
        return ctx.GetPath() == "/health"
    },
}))

Recovery

Catches panics and returns a 500 instead of crashing the server.

// simple — always register this first
k.Use(middleware.Recovery())

// custom — integrate with Sentry, Datadog, etc.
k.Use(middleware.RecoveryWithConfig(middleware.RecoveryConfig{
    OnPanic: func(ctx *kite.Context, recovered any, stack []byte) error {
        sentry.CaptureException(fmt.Errorf("%v", recovered))
        return ctx.WriteJSON(http.StatusInternalServerError, map[string]string{
            "error": "internal server error",
        })
    },
}))

RequestID

Attaches a unique ID to every request. Reuses an incoming X-Request-ID header if present, for distributed tracing.

// simple
k.Use(middleware.RequestID())

// custom header
k.Use(middleware.RequestIDWithConfig(middleware.RequestIDConfig{
    Header: "X-Trace-ID",
}))

// retrieve in handler
id := ctx.GetValue("X-Request-ID")

CORS

Sets Cross-Origin Resource Sharing headers and handles preflight requests.

// allow all origins
k.Use(middleware.CORS())

// restrict origins
k.Use(middleware.CORS("https://myapp.com"))

// full control
k.Use(middleware.CORSWithConfig(middleware.CORSConfig{
    AllowedOrigins:   []string{"https://myapp.com"},
    AllowedMethods:   []string{"GET", "POST", "PUT", "DELETE"},
    AllowedHeaders:   []string{"Content-Type", "Authorization"},
    ExposedHeaders:   []string{"X-Request-ID"},
    AllowCredentials: true,
    MaxAge:           86400,
}))

Note: AllowCredentials: true cannot be combined with AllowedOrigins: ["*"] — browsers will reject it.

MaxBodySize

Protects against oversized request bodies. Returns 413 if the limit is exceeded.

// global 4MB limit
k.Use(middleware.MaxBodySize(4 << 20))

// per-route override for file uploads
k.POST("/upload", uploadHandler, middleware.MaxBodySize(100 << 20))

Context

Every handler receives a *kite.Context with the following methods:

Request

Method Description
GetRequest() *http.Request Underlying request
GetResponse() http.ResponseWriter Underlying response writer
GetContext() context.Context Underlying context
GetMethod() string HTTP method
GetPath() string Request path
IsMethod(method string) bool Check HTTP method
GetPathParam(key string) string Route parameter (e.g. /users/:id)
GetQueryParam(key string) string Query string parameter
GetHeader(key string) string Request header
GetIP() string Client IP, respects X-Forwarded-For
GetCookie(name string) (*http.Cookie, error) Request cookie
GetStatusCode() int Current response status code

Values

Method Description
SetValue(key string, v any) Store a value in the context
GetValue(key string) any Retrieve a stored value

Forms

Method Description
GetForm() (url.Values, error) All form values
GetFormValue(key string) string Single form value
GetMultipartForm(maxMemory int64) (*multipart.Form, error) Multipart form
GetMultipartFormFile(key string) (multipart.File, *multipart.FileHeader, error) Uploaded file

Body Binding

Method Description
BindJSON(v any) error Decode JSON request body
BindXML(v any) error Decode XML request body

Response

Method Description
WriteBytes(status int, v []byte) error Raw bytes response
WriteString(status int, v string) error Plain text response
WriteJSON(status int, v any) error JSON response
WriteXML(status int, v any) error XML response
WriteNoContent() error 204 No Content
Redirect(status int, url string) error Redirect
SetHeader(key, value string) Set response header
SetCookie(cookie *http.Cookie) Set response cookie

Error Handling

Handlers return error. By default, errors are logged and a 500 is returned to the client.

k.GET("/users/:id", func(ctx *kite.Context) error {
    user, err := db.GetUser(ctx.GetPathParam("id"))
    if err != nil {
        return err // caught by error handler
    }
    return ctx.WriteJSON(http.StatusOK, user)
})

Custom Error Handler

k.SetErrorHandler(func(ctx *kite.Context, err error) error {
    return ctx.WriteJSON(http.StatusInternalServerError, map[string]string{
        "error": err.Error(),
    })
})

Custom Not Found Handler

k.SetNotFoundHandler(func(ctx *kite.Context) error {
    return ctx.WriteJSON(http.StatusNotFound, map[string]string{
        "error": "route not found",
    })
})

Custom Method Not Allowed Handler

k.SetMethodNotAllowedHandler(func(ctx *kite.Context) error {
    return ctx.WriteJSON(http.StatusMethodNotAllowed, map[string]string{
        "error": "method not allowed",
    })
})

Server Configuration

k := kite.New()

k.SetServerReadTimeout(10 * time.Second)     // default: 10s
k.SetServerWriteTimeout(30 * time.Second)    // default: 30s
k.SetServerIdleTimeout(60 * time.Second)     // default: 60s
k.SetServerShutdownTimeout(30 * time.Second) // default: 30s

Graceful Shutdown

Start() blocks until SIGINT or SIGTERM is received, then waits for all in-flight requests to finish before exiting. This maps directly to how Docker, Kubernetes, and systemd stop processes.

if err := k.Start("localhost:8080"); err != nil {
    log.Fatal(err)
}

TLS

Use StartTLS instead of Start and provide your certificate and key files:

if err := k.StartTLS("localhost:443", "cert.pem", "key.pem"); err != nil {
    log.Fatal(err)
}

For local development you can generate a self-signed certificate:

openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes

For production, use a certificate from a trusted CA. Caddy and Nginx are both good options as a reverse proxy if you prefer to terminate TLS outside the application.

Performance

Coming Soon — In-depth benchmarks including requests/sec, avg latency, RAM usage, and CPU utilization across varying concurrency levels, with comparisons against other Go frameworks.

License

MIT