# Backend API Patterns

## Handler Pattern (Wrap)

All handlers return `error` and are wrapped with `app.Wrap()`:

```go
func (h *Handler) Get(w http.ResponseWriter, r *http.Request) error {
    id := chi.URLParam(r, "id")
    item, err := h.svc.Get(r.Context(), id)
    if err != nil {
        if errors.Is(err, ErrNotFound) {
            return httperror.NotFoundf("item %s not found", id)
        }
        return err // becomes 500
    }
    httpresponse.OK(w, r, item)
    return nil
}

// In routes.go:
r.Get("/items/{id}", app.Wrap(handler.Get))
```

## Request Binding

Use `app.Bind` or `app.BindAndValidate`:

```go
func (h *Handler) Create(w http.ResponseWriter, r *http.Request) error {
    var req CreateRequest
    if err := app.BindAndValidate(r, &req); err != nil {
        return err // returns 400 or 422 HTTPError
    }
    // req is decoded and validated
}
```

Validation uses go-playground/validator struct tags:
- `validate:"required"` - field is required
- `validate:"min=1,max=100"` - length constraints
- `validate:"email"` - email format
- `validate:"uuid"` - UUID format

## HTTPError Sentinels

Use `httperror` factories to return typed errors:

| Function | Status | When to use |
|----------|--------|-------------|
| `httperror.BadRequest(msg)` | 400 | Invalid input format |
| `httperror.Unauthorized(msg)` | 401 | Missing/invalid credentials |
| `httperror.Forbidden(msg)` | 403 | No permission |
| `httperror.NotFoundf(fmt, args)` | 404 | Resource doesn't exist |
| `httperror.Conflict(msg)` | 409 | Duplicate resource |
| `httperror.Validation(msg)` | 422 | Struct validation failure |
| `httperror.Internal(msg)` | 500 | Server error (prefer returning raw err) |

Add details with `httperror.WithDetails(err, details)`.

## Response Envelope

All responses use the standard envelope from `httpresponse`:

```json
{
  "data": { ... },
  "meta": {
    "request_id": "abc-123",
    "timestamp": "2024-01-15T10:30:00Z"
  }
}
```

Use `httpresponse.OK(w, r, data)`, `httpresponse.Created(w, r, data)`, `httpresponse.NoContent(w)`.

## OpenAPI Documentation

Annotate endpoints in a `spec.go` file:

```go
spec := openapi.NewOpenAPISpec("Service Name", "1.0.0").
    WithBearerSecurity("bearer", "JWT token")

spec.WithSchema("Item", openapi.Object(map[string]openapi.Schema{
    "id":   openapi.UUID(),
    "name": openapi.String().WithExample("My Item"),
}, "id", "name"))

spec.AddPath("/api/v1/items", "get", map[string]any{
    "summary": "List items",
    "tags":    []string{"Items"},
    "responses": map[string]any{
        "200": openapi.OpResponse("Success", openapi.RefArray("Item")),
    },
})
```

Mount with `application.EnableDocs(spec)` to get `/docs` (Scalar UI) and `/openapi.json`.

## Auth Integration

Auth is opt-in via `AUTH_ENABLED=true`:

```go
// In routes.go - protected route group
r.Group(func(r app.Router) {
    if cfg.AuthEnabled {
        r.Use(auth.Middleware(auth.MiddlewareConfig{
            Validator: auth.NewJWTValidator(auth.JWTConfig{
                Secret: []byte(cfg.JWTSecret),
            }),
        }))
    }
    r.Post("/items", app.Wrap(handler.Create))
})
```

Access user in handlers:
```go
user := auth.GetUser(r.Context())
if user != nil {
    logger.Info("created by", "user", user.ID)
}
```

## Health Checks

Basic health is auto-registered at `/health` and `/ready`.

For dependency checks:
```go
healthHandler := app.NewHealthHandler(app.HealthConfig{
    Service: "my-service",
    Checks: map[string]app.HealthChecker{
        "database": app.PingChecker(db.PingContext),
        "redis":    app.PingChecker(redis.Ping),
    },
})
r.Get("/health", healthHandler)
```

## Chassis Package

The `pkg/chassis` package re-exports `pkg/app` types for convenience:

```go
import "git.threesix.ai/jordan/slack5-1770541397/pkg/chassis"

svc := chassis.New("my-service", chassis.WithDefaultPort(8080))
```
