docs: cerrar issue 0009 — HTTP server functions
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
# 0009 — HTTP Server Foundation
|
||||
|
||||
## Metadata
|
||||
|
||||
| Campo | Valor |
|
||||
|-------|-------|
|
||||
| **ID** | 0009 |
|
||||
| **Estado** | pendiente |
|
||||
| **Prioridad** | alta |
|
||||
| **Tipo** | feature |
|
||||
|
||||
## Dependencias
|
||||
|
||||
Ninguna.
|
||||
|
||||
---
|
||||
|
||||
## Objetivo
|
||||
|
||||
Crear funciones reutilizables de HTTP server en Go (dominio infra) que permitan montar una API REST completa componiendo primitivas del registry, en vez de construir el serving desde cero en cada app.
|
||||
|
||||
## Contexto
|
||||
|
||||
- `deploy_server` y `sqlite_api` construyen su HTTP serving ad-hoc cada vez: router manual con `http.ServeMux`, middleware inline, helpers de respuesta repetidos.
|
||||
- Existen funciones HTTP **client** (`http_get_json_go_infra`, `http_post_json_go_infra`) pero **CERO** funciones de HTTP **server**.
|
||||
- Go stdlib `net/http` es suficiente como base — no se necesita framework externo, solo primitivas componibles encima de stdlib.
|
||||
- Con estas funciones, una app nueva que necesite API solo hace: registrar rutas + componer middlewares + `http_serve`.
|
||||
|
||||
## Arquitectura
|
||||
|
||||
```
|
||||
functions/infra/
|
||||
├── http_router.go — NEW: registro de rutas con path params y métodos
|
||||
├── http_router.md — NEW
|
||||
├── http_middleware_chain.go — NEW: composición de middlewares
|
||||
├── http_middleware_chain.md — NEW
|
||||
├── http_cors_middleware.go — NEW: middleware CORS configurable
|
||||
├── http_cors_middleware.md — NEW
|
||||
├── http_logger_middleware.go — NEW: middleware de logging request/response
|
||||
├── http_logger_middleware.md — NEW
|
||||
├── http_json_response.go — NEW: helper para escribir JSON responses
|
||||
├── http_json_response.md — NEW
|
||||
├── http_error_response.go — NEW: helper para escribir error responses estandar
|
||||
├── http_error_response.md — NEW
|
||||
├── http_parse_body.go — NEW: decode JSON body con validación de tamaño
|
||||
├── http_parse_body.md — NEW
|
||||
├── http_serve.go — NEW: ListenAndServe con graceful shutdown
|
||||
├── http_serve.md — NEW
|
||||
|
||||
types/infra/
|
||||
├── http_route.md — NEW: metadata del tipo Route
|
||||
├── http_middleware.md — NEW: metadata del tipo Middleware
|
||||
├── http_error.md — NEW: metadata del tipo HTTPError
|
||||
```
|
||||
|
||||
### Patrón pure core / impure shell
|
||||
|
||||
- **Pure:** `http_middleware_chain` (composición de funciones), `http_cors_middleware` (retorna función sin I/O)
|
||||
- **Impure:** `http_router`, `http_json_response`, `http_error_response`, `http_parse_body`, `http_logger_middleware`, `http_serve` — todos interactúan con `http.ResponseWriter` / `http.Request` / red
|
||||
|
||||
## Diseño
|
||||
|
||||
### Tipos
|
||||
|
||||
```go
|
||||
// Middleware es un wrapper de http.Handler
|
||||
type Middleware func(http.Handler) http.Handler
|
||||
|
||||
// Route define una ruta con método y handler
|
||||
type Route struct {
|
||||
Method string
|
||||
Path string
|
||||
Handler http.HandlerFunc
|
||||
}
|
||||
|
||||
// HTTPError es un error estructurado para respuestas API
|
||||
type HTTPError struct {
|
||||
Status int `json:"status"`
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
```
|
||||
|
||||
### Funciones
|
||||
|
||||
| Función | Purity | Firma (simplificada) |
|
||||
|---------|--------|---------------------|
|
||||
| `http_router` | impure | `(routes []Route) *http.ServeMux` |
|
||||
| `http_middleware_chain` | pure | `(middlewares ...Middleware) Middleware` |
|
||||
| `http_cors_middleware` | pure | `(origins []string, methods []string) Middleware` |
|
||||
| `http_logger_middleware` | impure | `(logger io.Writer) Middleware` |
|
||||
| `http_json_response` | impure | `(w http.ResponseWriter, status int, data any)` |
|
||||
| `http_error_response` | impure | `(w http.ResponseWriter, err HTTPError)` |
|
||||
| `http_parse_body` | impure | `(r *http.Request, dst any, maxBytes int64) error` |
|
||||
| `http_serve` | impure | `(addr string, handler http.Handler, ctx context.Context) error` |
|
||||
|
||||
## Tareas
|
||||
|
||||
### Fase 1: Tipos
|
||||
|
||||
- [ ] **1.1** Crear tipos `Route`, `Middleware`, `HTTPError` en `functions/infra/` con `.md` en `types/infra/`
|
||||
|
||||
### Fase 2: Funciones puras
|
||||
|
||||
- [ ] **2.1** `http_middleware_chain` — compone N middlewares en uno solo (reduce de derecha a izquierda)
|
||||
- [ ] **2.2** `http_cors_middleware` — retorna Middleware que setea headers CORS según config
|
||||
|
||||
### Fase 3: Funciones impuras
|
||||
|
||||
- [ ] **3.1** `http_json_response` — serializa `data` a JSON, setea Content-Type, escribe status
|
||||
- [ ] **3.2** `http_error_response` — escribe HTTPError como JSON response
|
||||
- [ ] **3.3** `http_parse_body` — lee body con limit de tamaño, decode JSON, cierra body
|
||||
- [ ] **3.4** `http_logger_middleware` — loguea método, path, status, duración de cada request
|
||||
- [ ] **3.5** `http_router` — crea `http.ServeMux` y registra rutas con sus handlers
|
||||
- [ ] **3.6** `http_serve` — `http.Server` con graceful shutdown vía context cancelable + señales OS
|
||||
|
||||
### Fase 4: Tests y cleanup
|
||||
|
||||
- [ ] **4.1** Tests para cada función con `httptest.NewRecorder`
|
||||
- [ ] **4.2** `fn index` y verificar que todas las funciones aparecen en registry.db
|
||||
- [ ] **4.3** Verificar `go vet -tags fts5`
|
||||
|
||||
---
|
||||
|
||||
## Ejemplo de uso
|
||||
|
||||
```go
|
||||
// En cualquier app nueva:
|
||||
routes := []infra.Route{
|
||||
{Method: "GET", Path: "/health", Handler: healthHandler},
|
||||
{Method: "GET", Path: "/api/items", Handler: listItems},
|
||||
{Method: "POST", Path: "/api/items", Handler: createItem},
|
||||
}
|
||||
|
||||
mux := infra.HttpRouter(routes)
|
||||
|
||||
middleware := infra.HttpMiddlewareChain(
|
||||
infra.HttpCorsMiddleware([]string{"*"}, []string{"GET", "POST"}),
|
||||
infra.HttpLoggerMiddleware(os.Stdout),
|
||||
)
|
||||
|
||||
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||
defer cancel()
|
||||
|
||||
infra.HttpServe(":8080", middleware(mux), ctx)
|
||||
```
|
||||
|
||||
```go
|
||||
// Dentro de un handler:
|
||||
func listItems(w http.ResponseWriter, r *http.Request) {
|
||||
items, err := db.GetItems()
|
||||
if err != nil {
|
||||
infra.HttpErrorResponse(w, infra.HTTPError{Status: 500, Code: "db_error", Message: err.Error()})
|
||||
return
|
||||
}
|
||||
infra.HttpJsonResponse(w, 200, items)
|
||||
}
|
||||
|
||||
func createItem(w http.ResponseWriter, r *http.Request) {
|
||||
var input CreateItemInput
|
||||
if err := infra.HttpParseBody(r, &input, 1<<20); err != nil {
|
||||
infra.HttpErrorResponse(w, infra.HTTPError{Status: 400, Code: "bad_request", Message: err.Error()})
|
||||
return
|
||||
}
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## Decisiones de diseño
|
||||
|
||||
- **Solo stdlib `net/http`:** sin chi, gin, echo. Las funciones wrappean stdlib para mantener zero-dependency.
|
||||
- **`http.ServeMux` nativo:** Go 1.22+ soporta path params y métodos en `ServeMux`, suficiente sin router externo.
|
||||
- **Middleware como `func(http.Handler) http.Handler`:** patrón estándar de Go, compatible con cualquier middleware de terceros.
|
||||
- **Graceful shutdown con context:** permite que la app controle cuándo parar (señales OS, context padre, etc.).
|
||||
- **HTTPError como struct simple:** no implementa `error` interface — es un DTO de respuesta, no un error Go.
|
||||
|
||||
## Prerequisitos
|
||||
|
||||
Ninguno. Solo Go stdlib.
|
||||
|
||||
## Riesgos
|
||||
|
||||
- **Scope creep hacia un framework:** Mitigado manteniendo cada función atómica y sin estado compartido. No es un framework, son funciones sueltas.
|
||||
- **Colisión con patterns existentes en apps:** Las apps existentes pueden migrar gradualmente, no hay breaking change.
|
||||
Reference in New Issue
Block a user