152 lines
3.8 KiB
Markdown
152 lines
3.8 KiB
Markdown
# 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/sp4-fresh/pkg/chassis"
|
|
|
|
svc := chassis.New("my-service", chassis.WithDefaultPort(8080))
|
|
```
|