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
This commit is contained in:
390
README.md
390
README.md
@@ -1,2 +1,392 @@
|
||||
# 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
|
||||
|
||||
Reference in New Issue
Block a user