328 lines
5.0 KiB
Markdown
328 lines
5.0 KiB
Markdown
# Tower
|
|
|
|
Tower is a minimal, composable HTTP web framework for Go built on top of `net/http` and `httprouter`.
|
|
It provides a small, explicit API for routing, middleware composition, grouping, and structured request/response handling via a custom context.
|
|
|
|
The framework favors:
|
|
- Small surface area
|
|
- Explicit middleware chaining
|
|
- Typed handler signatures
|
|
- Zero magic abstractions
|
|
- Compatibility with the standard library
|
|
|
|
---
|
|
|
|
## Installation
|
|
|
|
```bash
|
|
go get git.trcreatives.at/TR_Creatives/go-tower
|
|
````
|
|
|
|
---
|
|
|
|
## Quick Start
|
|
|
|
```go
|
|
package main
|
|
|
|
import (
|
|
"log"
|
|
|
|
"git.trcreatives.at/TR_Creatives/go-tower"
|
|
)
|
|
|
|
func main() {
|
|
app := tower.New()
|
|
|
|
app.GET("/hello/:name", func(c *tower.Context) error {
|
|
name := c.Param("name")
|
|
return c.WriteJSON(200, tower.Map{
|
|
"message": "Hello " + name,
|
|
})
|
|
})
|
|
|
|
log.Fatal(app.Start(":8080"))
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Core Concepts
|
|
|
|
### Handler
|
|
|
|
All route handlers use a unified signature:
|
|
|
|
```go
|
|
type Handler func(c *Context) error
|
|
```
|
|
|
|
Returning an error forwards it to the configured error handler.
|
|
|
|
```go
|
|
app.GET("/ping", func(c *tower.Context) error {
|
|
return c.WriteString(200, "pong")
|
|
})
|
|
```
|
|
|
|
---
|
|
|
|
### Middleware
|
|
|
|
Middleware wraps handlers and composes into a chain:
|
|
|
|
```go
|
|
type Middleware func(h Handler) Handler
|
|
```
|
|
|
|
Example:
|
|
|
|
```go
|
|
func Logger() tower.Middleware {
|
|
return func(next tower.Handler) tower.Handler {
|
|
return func(c *tower.Context) error {
|
|
err := next(c)
|
|
log.Printf("%s %s -> %d",
|
|
c.Request().Method,
|
|
c.Request().URL.Path,
|
|
c.Status(),
|
|
)
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
Register globally:
|
|
|
|
```go
|
|
app.Use(Logger())
|
|
```
|
|
|
|
Or per route:
|
|
|
|
```go
|
|
app.GET("/secure", handler, AuthMiddleware())
|
|
```
|
|
|
|
Middleware execution order (outer → inner):
|
|
|
|
1. Global middleware
|
|
2. Group middleware
|
|
3. Route middleware
|
|
4. Handler
|
|
|
|
---
|
|
|
|
## Routing
|
|
|
|
Tower uses `httprouter`, so parameters are declared with `:name`.
|
|
|
|
```go
|
|
app.GET("/users/:id", func(c *tower.Context) error {
|
|
id := c.Param("id")
|
|
return c.WriteString(200, "User: "+id)
|
|
})
|
|
```
|
|
|
|
Available helpers:
|
|
|
|
```go
|
|
app.GET(path, handler, mws...)
|
|
app.POST(path, handler, mws...)
|
|
app.PUT(path, handler, mws...)
|
|
app.PATCH(path, handler, mws...)
|
|
app.DELETE(path, handler, mws...)
|
|
```
|
|
|
|
Or manually:
|
|
|
|
```go
|
|
app.Handle("GET", "/custom", handler)
|
|
```
|
|
|
|
---
|
|
|
|
## Groups
|
|
|
|
Groups allow route prefixing and shared middleware.
|
|
|
|
```go
|
|
api := app.Group("/api")
|
|
|
|
api.GET("/health", func(c *tower.Context) error {
|
|
return c.WriteString(200, "ok")
|
|
})
|
|
```
|
|
|
|
Nested groups:
|
|
|
|
```go
|
|
v1 := api.Group("/v1")
|
|
v1.GET("/users", listUsers)
|
|
```
|
|
|
|
Group-level middleware:
|
|
|
|
```go
|
|
auth := app.Group("/auth", AuthMiddleware())
|
|
auth.GET("/profile", profileHandler)
|
|
```
|
|
|
|
---
|
|
|
|
## Static Files
|
|
|
|
Serve a directory under a URL prefix:
|
|
|
|
```go
|
|
app.Static("/assets", "./public")
|
|
```
|
|
|
|
Group variant:
|
|
|
|
```go
|
|
api.Static("/docs", "./docs")
|
|
```
|
|
|
|
---
|
|
|
|
## Context API
|
|
|
|
`Context` wraps `http.Request` and `http.ResponseWriter` and provides helpers.
|
|
|
|
### Request Access
|
|
|
|
```go
|
|
c.Request() // *http.Request
|
|
c.Param("id") // path parameter
|
|
c.Query("q") // query string value
|
|
c.Header("X-Token") // request header
|
|
c.Cookie("session") // cookie
|
|
c.ClientIP() // best-effort client IP
|
|
```
|
|
|
|
### Response Helpers
|
|
|
|
```go
|
|
c.SetStatus(201)
|
|
c.SetHeader("X-App", "tower")
|
|
|
|
c.WriteString(200, "ok")
|
|
c.WriteJSON(200, data)
|
|
c.WriteXML(200, data)
|
|
c.WriteBytes(200, []byte("raw"), "application/octet-stream")
|
|
c.Redirect(302, "/login")
|
|
```
|
|
|
|
### Binding Request Bodies
|
|
|
|
```go
|
|
var payload CreateUserRequest
|
|
if err := c.BindJSON(&payload); err != nil {
|
|
return err
|
|
}
|
|
```
|
|
|
|
Also supported:
|
|
|
|
```go
|
|
c.BindXML(&payload)
|
|
```
|
|
|
|
### Context Values
|
|
|
|
```go
|
|
type userKey tower.ContextKey // define your own alias if desired
|
|
|
|
c.SetValue("userID", "123")
|
|
id := c.Value("userID")
|
|
```
|
|
|
|
(Values are stored in the underlying `request.Context()`.)
|
|
|
|
---
|
|
|
|
## Error Handling
|
|
|
|
Handlers return errors which are passed to the global error handler.
|
|
|
|
Set a custom handler:
|
|
|
|
```go
|
|
app.SetErrorHandler(func(c *tower.Context, err error) error {
|
|
return c.WriteJSON(500, tower.Map{
|
|
"error": err.Error(),
|
|
})
|
|
})
|
|
```
|
|
|
|
If the error handler itself returns an error, it is logged.
|
|
|
|
---
|
|
|
|
## Server Lifecycle
|
|
|
|
### Start HTTP
|
|
|
|
```go
|
|
if err := app.Start(":8080"); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
```
|
|
|
|
### Start HTTPS
|
|
|
|
```go
|
|
log.Fatal(app.StartTLS(":8443", "cert.pem", "key.pem"))
|
|
```
|
|
|
|
### Graceful Shutdown
|
|
|
|
```go
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
_ = app.Shutdown(ctx)
|
|
```
|
|
|
|
---
|
|
|
|
## Execution Flow
|
|
|
|
For each incoming request:
|
|
|
|
1. Route matched via `httprouter`
|
|
2. `Context` constructed
|
|
3. Route middleware applied
|
|
4. Global middleware applied
|
|
5. Handler executed
|
|
6. Returned error passed to `ErrorHandler`
|
|
|
|
---
|
|
|
|
## Map Helper
|
|
|
|
Tower provides a small convenience type:
|
|
|
|
```go
|
|
type Map map[string]any
|
|
```
|
|
|
|
Useful for JSON responses:
|
|
|
|
```go
|
|
return c.WriteJSON(200, tower.Map{
|
|
"status": "ok",
|
|
})
|
|
```
|
|
|
|
---
|
|
|
|
## Design Goals
|
|
|
|
* Minimal abstraction over `net/http`
|
|
* Deterministic middleware composition
|
|
* No reflection or hidden state
|
|
* Explicit error propagation
|
|
* Fully standard-library compatible |