No description
Find a file
2026-07-08 18:31:00 +02:00
middleware go-kite: v1.0.0 2026-06-14 21:50:49 +02:00
.gitignore Initial commit 2026-06-14 12:19:05 +02:00
context.go go-kite: v1.2.0 2026-07-08 14:43:50 +02:00
error_handler.go go-kite: v1.0.0 2026-06-14 21:50:49 +02:00
go.mod go-kite: v1.2.0 2026-07-08 14:43:50 +02:00
go.sum go-kite: v1.2.0 2026-07-08 14:43:50 +02:00
group.go go-kite: v1.0.0 2026-06-14 21:50:49 +02:00
handler.go go-kite: v1.0.0 2026-06-14 21:50:49 +02:00
kite.go go-kite: v1.3.0 2026-07-08 18:31:00 +02:00
LICENSE go-kite: v1.0.0 2026-06-14 21:50:49 +02:00
map.go go-kite: v1.2.0 2026-07-08 14:43:50 +02:00
method_not_allowed_handler.go go-kite: v1.0.0 2026-06-14 21:50:49 +02:00
middleware.go go-kite: v1.0.0 2026-06-14 21:50:49 +02:00
not_found_handler.go go-kite: v1.0.0 2026-06-14 21:50:49 +02:00
README.md go-kite: v1.3.0 2026-07-08 18:31:00 +02:00
response_writer.go go-kite: v1.0.0 2026-06-14 21:50:49 +02:00

🪁 Kite — A Fast, Lightweight Go HTTP Framework

Kite is a high-performance, minimalist HTTP framework for Go built on top of httprouter. It provides an elegant, Express-like API with first-class support for middleware, route grouping, WebSockets, static files, SPAs, and graceful shutdown — all with zero unnecessary allocations.


Table of Contents


Installation

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

Kite depends on three well-known, lightweight libraries:

Dependency Purpose
github.com/julienschmidt/httprouter High-performance radix-tree router
github.com/gorilla/websocket WebSocket support
github.com/gorilla/schema Form/multipart decoding

Quick Start

package main

import (
    "net/http"

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

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

    // Global middleware
    k.Use(middleware.Logger())
    k.Use(middleware.Recovery())
    k.Use(middleware.RequestID())

    // Routes
    k.GET("/", func(ctx *kite.Context) error {
        return ctx.WriteString(http.StatusOK, "Hello, World!")
    })

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

    k.Start(":8080")
}

Visit http://localhost:8080/Hello, World!
Visit http://localhost:8080/hello/Kite{"message":"Hello, Kite!"}


Core Concepts

Handler

Every route handler is a function with the signature:

type Handler func(ctx *Context) error

Returning a non-nil error triggers the framework's error handler, which by default writes a 500 response with the error message.

k.GET("/example", func(ctx *kite.Context) error {
    // Do work...
    return nil // or return an error
})

Middleware

Middleware wraps a Handler and returns a new Handler:

type Middleware func(h Handler) Handler

A middleware can:

  • Execute code before the handler
  • Execute code after the handler
  • Short-circuit the request (not call the next handler)
  • Modify the context
// Example: custom timing middleware
func TimingMiddleware() kite.Middleware {
    return func(h kite.Handler) kite.Handler {
        return func(ctx *kite.Context) error {
            start := time.Now()
            err := h(ctx)
            elapsed := time.Since(start)
            ctx.SetHeader("X-Response-Time", elapsed.String())
            return err
        }
    }
}

Context

The Context is the heart of every request. It provides:

  • Access to the underlying *http.Request and http.ResponseWriter
  • Path parameter and query parameter reading
  • Request body binding (JSON, XML, forms, multipart)
  • Response writing helpers (JSON, XML, string, stream, redirect)
  • Cookie and header manipulation
  • Per-request key/value store

Error Handling

When a handler returns an error, the framework calls the configured ErrorHandler:

type ErrorHandler func(ctx *Context, err error) error

The default writes 500 internal server error with err.Error() as the body. You can replace it:

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

Routing

Kite uses httprouter under the hood, giving you fast, radix-tree-based routing with zero allocations during path matching.

HTTP Method Helpers

All standard HTTP methods are available on both *Kite and *Group:

Method Helper
GET .GET(path, handler, middleware...)
POST .POST(path, handler, middleware...)
PUT .PUT(path, handler, middleware...)
PATCH .PATCH(path, handler, middleware...)
DELETE .DELETE(path, handler, middleware...)
HEAD .HEAD(path, handler, middleware...)
OPTIONS .OPTIONS(path, handler, middleware...)
CONNECT .CONNECT(path, handler, middleware...)
TRACE .TRACE(path, handler, middleware...)

Each accepts optional route-level middleware that runs after global/group middleware but before the handler.

Path Parameters

Kite supports :param and *wildcard syntax from httprouter:

// Named parameter
k.GET("/users/:id", func(ctx *kite.Context) error {
    id := ctx.PathParam("id") // "42" for /users/42
    return ctx.WriteString(200, "User: "+id)
})

// Wildcard (catch-all)
k.GET("/files/*filepath", func(ctx *kite.Context) error {
    path := ctx.PathParam("filepath") // "/css/style.css" for /files/css/style.css
    return ctx.WriteString(200, "File: "+path)
})

Route Groups

Groups allow you to share a common path prefix and middleware:

api := k.Group("/api")
api.Use(AuthMiddleware())

// These are all under /api and have AuthMiddleware applied
api.GET("/users", listUsers)
api.POST("/users", createUser)
api.GET("/users/:id", getUser)

// Nested groups
admin := api.Group("/admin")
admin.Use(AdminOnlyMiddleware())
admin.DELETE("/users/:id", deleteUser)

Group-level middleware is applied in order: parent group middleware first, then child group middleware, then route-level middleware.

Global Middleware

Applied to every route via k.Use():

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

Global middleware wraps the entire request pipeline, running before any group or route middleware.


Request Handling

Reading Query Parameters

k.GET("/search", func(ctx *kite.Context) error {
    q := ctx.QueryParam("q")       // ?q=hello → "hello"
    page := ctx.QueryParam("page") // ?page=2 → "2" (empty string if missing)
    return ctx.WriteString(200, "Search: "+q+", Page: "+page)
})

Reading Path Parameters

k.GET("/posts/:slug", func(ctx *kite.Context) error {
    slug := ctx.PathParam("slug")
    return ctx.WriteString(200, "Post: "+slug)
})

Reading Headers

k.GET("/headers", func(ctx *kite.Context) error {
    authorization := ctx.Header("Authorization")
    contentType := ctx.Header("Content-Type")
    return ctx.WriteJSON(200, map[string]string{
        "auth": authorization,
        "ct":   contentType,
    })
})

Reading Cookies

k.GET("/profile", func(ctx *kite.Context) error {
    cookie, err := ctx.Cookie("session_id")
    if err != nil {
        return ctx.WriteString(401, "Not authenticated")
    }
    return ctx.WriteString(200, "Session: "+cookie.Value)
})

Reading the Request Body

k.POST("/raw", func(ctx *kite.Context) error {
    body, err := ctx.Body()
    if err != nil {
        return err
    }
    return ctx.WriteString(200, "Received: "+string(body))
})

Form & Multipart Data

URL-encoded forms:

k.POST("/login", func(ctx *kite.Context) error {
    username := ctx.FormValue("username")
    password := ctx.FormValue("password")
    return ctx.WriteJSON(200, map[string]string{"user": username})
})

Multipart file uploads:

k.POST("/upload", func(ctx *kite.Context) error {
    // Parse multipart form with 32MB limit
    form, err := ctx.MultipartForm(32 << 20)
    if err != nil {
        return err
    }

    // Access files
    files := form.File["files"]
    for _, fh := range files {
        // Process fh...
    }
    return ctx.WriteNoContent()
})

// Or get a single file directly
k.POST("/avatar", func(ctx *kite.Context) error {
    file, header, err := ctx.MultipartFormFile("avatar")
    if err != nil {
        return err
    }
    defer file.Close()
    // Process file...
    return ctx.WriteString(200, "Uploaded: "+header.Filename)
})

Binding

The Bind method automatically detects the Content-Type and decodes the request body into a Go struct. Supported content types:

Content-Type Decoder
application/json json.Decoder
application/xml xml.Decoder
application/x-www-form-urlencoded gorilla/schema
multipart/form-data gorilla/schema
type CreateUserInput struct {
    Name  string `json:"name" schema:"name"`
    Email string `json:"email" schema:"email"`
    Age   int    `json:"age" schema:"age"`
}

k.POST("/users", func(ctx *kite.Context) error {
    var input CreateUserInput
    if err := ctx.Bind(&input); err != nil {
        return err
    }
    // input is populated
    return ctx.WriteJSON(201, input)
})

Individual binding methods are also available:

  • ctx.BindJSON(&v) — JSON only
  • ctx.BindXML(&v) — XML only
  • ctx.BindForm(&v) — Form / multipart
  • ctx.BindFormWithMaxMemory(&v, maxMemory) — Form with custom memory limit
  • ctx.BindWithMaxMemory(&v, maxMemory) — Auto-detect with custom memory limit

Client IP

Respects X-Forwarded-For and X-Real-IP headers for proxy-aware IP resolution:

k.GET("/ip", func(ctx *kite.Context) error {
    return ctx.WriteString(200, ctx.IP())
})

Fallback order: X-Forwarded-ForX-Real-IPRemoteAddr.


Response Handling

Writing Strings

ctx.WriteString(http.StatusOK, "Hello, World!")

Sets Content-Type: text/plain; charset=utf-8.

Writing JSON

ctx.WriteJSON(http.StatusOK, map[string]any{
    "status": "ok",
    "data":   someStruct,
})

Sets Content-Type: application/json; charset=utf-8.

Writing XML

ctx.WriteXML(http.StatusOK, someStruct)

Sets Content-Type: application/xml; charset=utf-8.

Writing Raw Bytes

ctx.WriteBytes(http.StatusOK, "image/png", imageBytes)

No Content (204)

ctx.WriteNoContent()

Writes a 204 No Content status with no body.

Streaming Responses

k.GET("/download", func(ctx *kite.Context) error {
    file, _ := os.Open("large-file.zip")
    defer file.Close()
    return ctx.Stream(http.StatusOK, "application/zip", file)
})

Copies from an io.Reader directly to the response writer.

Redirects

ctx.Redirect(http.StatusFound, "/new-location")

Setting Headers & Cookies

// Headers
ctx.SetHeader("X-Custom", "value")     // Set (overwrites)
ctx.AddHeader("X-Custom", "another")   // Add (appends)

// Cookies
ctx.SetCookie(&http.Cookie{
    Name:  "session",
    Value: "abc123",
    Path:  "/",
})
ctx.ClearCookie("session") // Removes a cookie by setting MaxAge=-1

Middleware (Built-in)

Kite ships with five production-ready middleware in the middleware package.

CORS

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

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

// With specific origins
k.Use(middleware.CORS("https://example.com", "https://app.example.com"))

// Full configuration
k.Use(middleware.CORSWithConfig(middleware.CORSConfig{
    AllowedOrigins:   []string{"https://example.com"},
    AllowedMethods:   []string{"GET", "POST", "PUT"},
    AllowedHeaders:   []string{"Content-Type", "Authorization", "X-API-Key"},
    ExposedHeaders:   []string{"X-Request-ID", "X-Total-Count"},
    AllowCredentials: true,
    MaxAge:           3600, // seconds
}))
Config Field Default Description
AllowedOrigins ["*"] Allowed origin patterns
AllowedMethods All 9 methods Allowed HTTP methods
AllowedHeaders ["Content-Type", "Authorization"] Allowed request headers
ExposedHeaders [] Headers exposed to the browser
AllowCredentials false Allow cookies/auth headers
MaxAge 0 Preflight cache duration (seconds)

Special behavior: When AllowedOrigins is ["*"] and AllowCredentials is true, the origin is echoed back instead of * (as required by the CORS spec). Preflight OPTIONS requests are handled automatically — the middleware returns 204 No Content.

Logger

// Default: logs to stdout
k.Use(middleware.Logger())

// Custom configuration
k.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
    Output: logFile, // io.Writer
    Format: func(method, path string, statusCode int, latency time.Duration) string {
        return fmt.Sprintf("%s %s → %d (%s)", method, path, statusCode, latency)
    },
    Skip: func(ctx *kite.Context) bool {
        return ctx.Path() == "/health" // don't log health checks
    },
}))

Default format: [Kite] GET /users/42 -> 200 in 1.234ms

Recovery (Panic Handling)

// Default: logs panic and returns 500
k.Use(middleware.Recovery())

// Custom panic handler
k.Use(middleware.RecoveryWithConfig(middleware.RecoveryConfig{
    OnPanic: func(ctx *kite.Context, recovered any, stack []byte) error {
        log.Printf("PANIC: %v\n%s", recovered, stack)
        return ctx.WriteJSON(500, map[string]string{
            "error": "Internal server error",
        })
    },
}))

Must be registered before other middleware to catch panics from the entire stack.

Request ID

// Default: uses X-Request-ID header, generates UUID-like ID if missing
k.Use(middleware.RequestID())

// Custom configuration
k.Use(middleware.RequestIDWithConfig(middleware.RequestIDConfig{
    Header: "X-Correlation-ID",
    Generator: func() string {
        return uuid.New().String()
    },
}))

The ID is both set as a response header and stored in the context, accessible via:

id := ctx.Value("X-Request-ID") // or whatever header name you configured

It reads an incoming ID from the request header if present, otherwise generates one.

Max Body Size

// Default: 4 MB
k.Use(middleware.MaxBodySize(4 << 20))

// Custom limit
k.Use(middleware.MaxBodySize(1 << 20)) // 1 MB
k.Use(middleware.MaxBodySizeWithConfig(middleware.MaxBodySizeConfig{
    MaxBytes: 10 << 20, // 10 MB
}))

Wraps the request body with http.MaxBytesReader. If a client exceeds the limit, reading the body will return an error.


Static Files & SPA

Serving Static Files

// Serve ./public at /static/
k.Static("/static", "./public")
// GET /static/style.css → ./public/style.css
// GET /static/js/app.js → ./public/js/app.js

// With custom http.FileSystem
k.StaticFS("/assets", http.Dir("./build"))

// With middleware (e.g., caching headers)
k.Static("/static", "./public", cacheMiddleware())

Single Page Application (SPA) Mode

Redirects all unmatched routes to index.html:

k.SPA("./dist")
// OR
k.SPAFS(http.Dir("./dist"))

Under the hood, this replaces the NotFoundHandler. If a file exists at the requested path, it is served; otherwise, the request is rewritten to / and served by the file server (typically hitting index.html).

⚠️ Calling SPA / SPAFS after SetNotFoundHandler will override it, and vice versa. Kite logs a warning when this happens.

Static Files on Groups

admin := k.Group("/admin")
admin.Static("/assets", "./admin-assets")
// Serves ./admin-assets at /admin/assets/

WebSockets

import "github.com/gorilla/websocket"

// Default upgrader (allows all origins)
k.WebSocket("/ws", func(conn *websocket.Conn) error {
    for {
        msgType, msg, err := conn.ReadMessage()
        if err != nil {
            return err // closes connection
        }
        conn.WriteMessage(msgType, msg) // echo
    }
})

// Custom upgrader
upgrader := websocket.Upgrader{
    ReadBufferSize:  4096,
    WriteBufferSize: 4096,
    CheckOrigin: func(r *http.Request) bool {
        return r.Header.Get("Origin") == "https://trusted.example.com"
    },
}
k.WebSocketWithUpgrader("/secure-ws", upgrader, handleWebSocket)

// With middleware (runs before upgrade)
k.WebSocket("/ws", handleWebSocket, authMiddleware())

WebSocket routes can also be defined on groups:

api := k.Group("/api")
api.WebSocket("/realtime", handleRealtime, authMiddleware())

Server Configuration

Timeouts

k := kite.New()
k.SetServerReadTimeout(30 * 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
Method Controls
SetServerReadTimeout Max time to read the entire request (body included)
SetServerWriteTimeout Max time to write the response
SetServerIdleTimeout Max idle time for keep-alive connections
SetServerShutdownTimeout Max time to wait for active connections during shutdown

Starting the Server

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

Start blocks until the server is shut down. It automatically handles SIGINT and SIGTERM for graceful shutdown.

TLS / HTTPS

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

Graceful Shutdown

Automatic on SIGINT/SIGTERM. For manual shutdown:

ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := k.Shutdown(ctx); err != nil {
    log.Printf("Shutdown error: %v", err)
}

Custom Handlers

Not Found (404)

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

Method Not Allowed (405)

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

Error Handler

k.SetErrorHandler(func(ctx *kite.Context, err error) error {
    // Log the error
    log.Printf("Error: %v", err)

    // Return structured JSON
    return ctx.WriteJSON(http.StatusInternalServerError, map[string]string{
        "error":   "Internal server error",
        "request": ctx.Header("X-Request-ID"),
    })
})

Complete Example

package main

import (
    "log"
    "net/http"
    "time"

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

type User struct {
    ID    int    `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
}

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

    // ── Server configuration ──
    k.SetServerReadTimeout(15 * time.Second)
    k.SetServerWriteTimeout(15 * time.Second)
    k.SetServerShutdownTimeout(10 * time.Second)

    // ── Global middleware ──
    k.Use(middleware.RequestID())
    k.Use(middleware.Logger())
    k.Use(middleware.Recovery())
    k.Use(middleware.CORS("http://localhost:3000"))
    k.Use(middleware.MaxBodySize(5 << 20)) // 5 MB

    // ── Custom error handler ──
    k.SetErrorHandler(func(ctx *kite.Context, err error) error {
        return ctx.WriteJSON(http.StatusInternalServerError, map[string]string{
            "error":       err.Error(),
            "request_id":  ctx.Value("X-Request-ID").(string),
        })
    })

    // ── Custom 404 ──
    k.SetNotFoundHandler(func(ctx *kite.Context) error {
        return ctx.WriteJSON(http.StatusNotFound, map[string]string{
            "error": "Not found",
        })
    })

    // ── Public routes ──
    k.GET("/health", func(ctx *kite.Context) error {
        return ctx.WriteJSON(200, map[string]string{"status": "ok"})
    })

    // ── API group ──
    api := k.Group("/api")

    // Auth middleware for API
    api.Use(func(h kite.Handler) kite.Handler {
        return func(ctx *kite.Context) error {
            if ctx.Header("Authorization") == "" {
                return ctx.WriteString(401, "Unauthorized")
            }
            return h(ctx)
        }
    })

    api.GET("/users", func(ctx *kite.Context) error {
        users := []User{
            {ID: 1, Name: "Alice", Email: "alice@example.com"},
            {ID: 2, Name: "Bob", Email: "bob@example.com"},
        }
        return ctx.WriteJSON(200, users)
    })

    api.POST("/users", func(ctx *kite.Context) error {
        var user User
        if err := ctx.BindJSON(&user); err != nil {
            return err
        }
        user.ID = 3
        return ctx.WriteJSON(201, user)
    })

    api.GET("/users/:id", func(ctx *kite.Context) error {
        id := ctx.PathParam("id")
        return ctx.WriteJSON(200, User{ID: 1, Name: "User " + id})
    })

    // ── WebSocket ──
    k.WebSocket("/ws", func(conn *websocket.Conn) error {
        for {
            mt, msg, err := conn.ReadMessage()
            if err != nil {
                return err
            }
            conn.WriteMessage(mt, msg)
        }
    })

    // ── Static files ──
    k.Static("/static", "./public")

    // ── Start server ──
    log.Fatal(k.Start(":8080"))
}

API Reference

Kite Struct

Method Description
New() *Kite Create a new Kite instance with sensible defaults
Start(addr string) error Start HTTP server with graceful shutdown
StartTLS(addr, certFile, keyFile string) error Start HTTPS server with graceful shutdown
Shutdown(ctx context.Context) error Manually shut down the server
ServeHTTP(w http.ResponseWriter, r *http.Request) Implements http.Handler for use with httptest
Use(mws ...Middleware) Register global middleware
GET/POST/PUT/PATCH/DELETE/HEAD/OPTIONS/CONNECT/TRACE(path, handler, mws...) Register a route
Group(prefix string, mws ...Middleware) *Group Create a route group
Static(prefix, root string, mws ...Middleware) Serve static files from a directory
StaticFS(prefix string, fs http.FileSystem, mws ...Middleware) Serve static files from an http.FileSystem
SPA(root string) SPA mode from a directory
SPAFS(fs http.FileSystem) SPA mode from an http.FileSystem
WebSocket(path string, onConnect func(*websocket.Conn) error, mws ...Middleware) WebSocket endpoint with default upgrader
WebSocketWithUpgrader(path string, upgrader websocket.Upgrader, onConnect func(*websocket.Conn) error, mws ...Middleware) WebSocket endpoint with custom upgrader
SetErrorHandler(eh ErrorHandler) Set the error handler
SetNotFoundHandler(nfh NotFoundHandler) Set the 404 handler
SetMethodNotAllowedHandler(mnah MethodNotAllowedHandler) Set the 405 handler
SetServerReadTimeout(d time.Duration) Set http.Server.ReadTimeout
SetServerWriteTimeout(d time.Duration) Set http.Server.WriteTimeout
SetServerIdleTimeout(d time.Duration) Set http.Server.IdleTimeout
SetServerShutdownTimeout(d time.Duration) Set max wait time during graceful shutdown

Context Struct

Request Methods

Method Returns Description
Request() *http.Request *http.Request Raw HTTP request
Response() http.ResponseWriter http.ResponseWriter Raw response writer
Context() context.Context context.Context Request context
Method() string string HTTP method
Path() string string Request URL path
StatusCode() int int Current response status code
PathParam(key string) string string URL path parameter
QueryParam(key string) string string URL query parameter
Header(key string) string string Request header value
Cookie(key string) (*http.Cookie, error) *http.Cookie, error Named cookie
Body() ([]byte, error) []byte, error Full request body
Form() (url.Values, error) url.Values, error Parsed form values
FormValue(key string) string string Single form value
MultipartForm(maxMemory int64) (*multipart.Form, error) *multipart.Form, error Parsed multipart form
MultipartFormFile(key string) (multipart.File, *multipart.FileHeader, error) File, header, error Single uploaded file
IP() string string Client IP (proxy-aware)
IsMethod(method string) bool bool Check HTTP method

Binding Methods

Method Description
Bind(v any) error Auto-detect Content-Type and decode (32MB form limit)
BindWithMaxMemory(v any, maxMemory int64) error Auto-detect with custom form memory limit
BindJSON(v any) error Decode JSON body
BindXML(v any) error Decode XML body
BindForm(v any) error Decode form/multipart (32MB limit)
BindFormWithMaxMemory(v any, maxMemory int64) error Decode form with custom memory limit

Response Methods

Method Description
WriteString(statusCode int, v string) error Write plain text response
WriteJSON(statusCode int, v any) error Write JSON response
WriteXML(statusCode int, v any) error Write XML response
WriteBytes(statusCode int, contentType string, v []byte) error Write raw bytes
WriteNoContent() error Write 204 No Content
Stream(statusCode int, contentType string, r io.Reader) error Stream from reader
Redirect(statusCode int, url string) error HTTP redirect
SetHeader(key, value string) Set response header (overwrites)
AddHeader(key, value string) Add response header (appends)
SetCookie(cookie *http.Cookie) Set a cookie
ClearCookie(key string) Remove a cookie
SetValue(key string, v any) Store value in request context
Value(key string) any Retrieve value from request context

Group Struct

Method Description
Use(mws ...Middleware) Add middleware to group
Group(prefix string, mws ...Middleware) *Group Create nested group
GET/POST/PUT/... (all 9 methods) Register route under group prefix
Static(prefix, root string, mws ...Middleware) Serve static files under group prefix
StaticFS(prefix string, fs http.FileSystem, mws ...Middleware) Serve static files from http.FileSystem
WebSocket(path string, onConnect func(*websocket.Conn) error, mws ...Middleware) WebSocket under group prefix
WebSocketWithUpgrader(...) WebSocket with custom upgrader under group prefix

Types

// Handler is a function that processes a request and returns an error.
type Handler func(ctx *Context) error

// Middleware wraps a Handler with cross-cutting logic.
type Middleware func(h Handler) Handler

// ErrorHandler handles errors returned by handlers.
type ErrorHandler func(ctx *Context, err error) error

// NotFoundHandler handles 404 responses.
type NotFoundHandler Handler

// MethodNotAllowedHandler handles 405 responses.
type MethodNotAllowedHandler Handler

Testing with httptest

Kite implements http.Handler via ServeHTTP, so you can test your routes without binding a real port:

k := kite.New()
k.GET("/hello", func(ctx *kite.Context) error {
    return ctx.WriteString(200, "Hello, World!")
})

ts := httptest.NewServer(k)      // no goroutines, no ports, no polling
defer ts.Close()

resp, _ := http.Get(ts.URL + "/hello")
// assert on resp.StatusCode, body, etc.
---

## License

MIT  see [LICENSE](./LICENSE) for details.