# go-kite A fast, lightweight, and expressive HTTP framework for Go, built on top of [httprouter](https://github.com/julienschmidt/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 ```bash go get git.trcreatives.at/trcreatives/go-kite ``` ## Quickstart ```go 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 ```go 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. ```go api := k.Group("/api") api.GET("/users", listUsers) // GET /api/users api.POST("/users", createUser) // POST /api/users ``` Groups can be nested: ```go api := k.Group("/api") v1 := api.Group("/v1") v1.GET("/users", listUsers) // GET /api/v1/users ``` Middleware can be attached to groups: ```go 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. ```go type Middleware func(h Handler) Handler ``` ### Global Middleware Applied to every route: ```go k.Use(middleware.Recovery()) k.Use(middleware.Logger()) ``` ### Route Middleware Applied to a single route: ```go 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 ```go 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: ```go import "git.trcreatives.at/trcreatives/go-kite/middleware" ``` ### Logger Logs method, path, status code and latency for every request. ```go // 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. ```go // 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. ```go // 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. ```go // 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. ```go // 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. ```go 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 ```go k.SetErrorHandler(func(ctx *kite.Context, err error) error { return ctx.WriteJSON(http.StatusInternalServerError, map[string]string{ "error": err.Error(), }) }) ``` ### Custom Not Found Handler ```go k.SetNotFoundHandler(func(ctx *kite.Context) error { return ctx.WriteJSON(http.StatusNotFound, map[string]string{ "error": "route not found", }) }) ``` ### Custom Method Not Allowed Handler ```go k.SetMethodNotAllowedHandler(func(ctx *kite.Context) error { return ctx.WriteJSON(http.StatusMethodNotAllowed, map[string]string{ "error": "method not allowed", }) }) ``` ## Server Configuration ```go 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. ```go if err := k.Start("localhost:8080"); err != nil { log.Fatal(err) } ``` ## TLS Use `StartTLS` instead of `Start` and provide your certificate and key files: ```go 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: ```bash 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](https://caddyserver.com) and [Nginx](https://nginx.org) 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