Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 1 addition & 10 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,7 @@ cmd/hyperfleet-api/ # Entry point + subcommands (serve, migrate)
environments/ # Environment configs (development, unit_testing, etc.)
pkg/
api/openapi/ # GENERATED — models + embedded spec (never edit)
handlers/ # HTTP handlers using handlerConfig pipeline
framework.go # handle/handleGet/handleList/handleDelete pipeline
handlers/ # HTTP handler pattern, validation and error handling
services/ # Service interfaces + sqlXxxService implementations
dao/ # DAO interfaces + sqlXxxDao implementations
db/ # SessionFactory, transaction middleware, migrations
Expand Down Expand Up @@ -125,14 +124,6 @@ Use constructor functions from `pkg/errors/errors.go`: `NotFound()`, `Validation

Use `pkg/logger/` — `logger.Info(ctx, "msg")`, `logger.With(ctx, "key", val).Error("msg")`. Never use `fmt.Println` or `log.Print`.

### Handlers

Use `handlerConfig` pipeline from `pkg/handlers/framework.go`:

- `handle(w, r, cfg, status)` — unmarshal → validate → action → respond
- `handleGet/handleList/handleDelete` — no-body variants
- Validation: `func() *errors.ServiceError`; Action: `func() (interface{}, *errors.ServiceError)`

### Services

Interface + `sql*Service` struct. Constructor injection of DAOs. Return `*errors.ServiceError`. Add `//go:generate mockgen` directive for mocks.
Expand Down
10 changes: 5 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,10 @@ Use the structured logging API — never `fmt.Println` or `log.Print`:
- Chainable: `logger.With(ctx, "key", value).WithError(err).Error("failed")`

### Handler Pipeline
Handlers use the `handlerConfig` pipeline — Reference: `pkg/handlers/framework.go`
- `handle()` — full pipeline: unmarshal body → validate → action → respond
- `handleGet()` / `handleList()` / `handleDelete()` — no body variants
- Validation functions return `*errors.ServiceError`; action functions return `(interface{}, *errors.ServiceError)`
HTTP handlers (ResourceHandler, RootResourceHandler) are thin orchestration layers:
- Reference: `pkg/handlers/`
- Decode and validate requests, call services, marshal responses, handle errors
- No business logic — that lives in `pkg/services/`

### Service Pattern
Interface + `sql*Service` implementation with constructor injection:
Expand Down Expand Up @@ -117,7 +117,7 @@ No per-entity Go code needed. See `plugins/CLAUDE.md` for details.

Subdirectories contain context-specific guidance that loads when you work in those areas:

- `pkg/handlers/CLAUDE.md` — Handler pipeline and handlerConfig patterns
- `pkg/handlers/CLAUDE.md` — Handler patterns, validation, and error handling
- `pkg/services/CLAUDE.md` — Service interface and status aggregation patterns
- `pkg/dao/CLAUDE.md` — DAO interface, session access, and rollback patterns
- `pkg/db/CLAUDE.md` — SessionFactory and transaction middleware
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ hyperfleet-api/
│ ├── dao/ # Data access layer (database interactions)
│ ├── db/ # Database session factory, migrations, transaction middleware
│ ├── errors/ # RFC 9457 Problem Details error model
│ ├── handlers/ # HTTP request handlers using handlerConfig pipeline
│ ├── handlers/ # HTTP handler pattern, validation and error handling
│ ├── logger/ # Structured logging (slog-based)
│ ├── presenters/ # Response presenters (DAO models → API responses)
│ └── services/ # Business logic layer (status aggregation, validation)
Expand Down
122 changes: 101 additions & 21 deletions pkg/handlers/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,35 +1,115 @@
# Claude Code Guidelines for Handlers

Handlers use the `handlerConfig` pipeline defined in `framework.go`.
## Request Processing

## Pipeline Functions
Handlers follow a standard pipeline:

- `handle(w, r, cfg, httpStatus)` — Full pipeline: unmarshal `cfg.MarshalInto` from request body → run `cfg.Validate` functions → execute `cfg.Action` → write response
- `handleGet(w, r, cfg)` — No body read, action only, returns 200
- `handleList(w, r, cfg)` — No body read, action only, returns 200
- `handleDelete(w, r, cfg)` — No body read, action only, returns 204
- `handleCreateWithNoContent(w, r, cfg)` — Body read + validate, returns 204 if action returns nil
1. **Decode and validate** — Use `decodeAndValidate(r, &req, validateFuncs)` to unmarshal request body and run validation
- Pass `"strict"` as 4th parameter for PATCH requests only to reject unknown/immutable fields via `DisallowUnknownFields()`
- Validation functions return `*errors.ServiceError` on failure
2. **Convert** — Use `presenters.Convert*()` to convert request types to internal models
3. **Call service** — Pass converted models to service layer methods
4. **Present** — Use `presenters.Present*()` to convert internal models to response types
5. **Write response** — Use `writeJSONResponse(w, r, statusCode, data)` or `handleError(r, w, err)`

## handlerConfig Fields
## Handler Types

- `MarshalInto`: pointer to struct for JSON unmarshalling (e.g., `&openapi.ResourceCreateRequest{}`)
- `Validate`: slice of `func() *errors.ServiceError` — each returns nil on success
- `Action`: `func() (interface{}, *errors.ServiceError)` — core business logic
- `ErrorHandler`: optional custom error handler (rarely used)
- **ResourceHandler** — Handles both flat (`/api/hyperfleet/v1/clusters`) and owner-nested (`/api/hyperfleet/v1/clusters/{parent_id}/nodepools`) routes for a single entity kind
- Extract parent_id to determine route type - `r.PathValue("parent_id")`
- See struct comment for safety invariant (plugins must register correctly)
- **RootResourceHandler** — Handles kind-agnostic routes (`/api/hyperfleet/v1/resources`, `/api/hyperfleet/v1/resources/{id}`)
- Resolves entity kind dynamically via `registry.Get(kind)`
- Handles adapter status operations (`/api/hyperfleet/v1/resources/{id}/statuses`)

## Patterns
## Validation Patterns

- Validation helpers check fields on the unmarshalled struct and return `errors.Validation("reason")` on failure
- Use `presenters.Convert*()` to convert request types to internal models before calling services
- Use `presenters.Present*()` to convert internal models to response types
- Handler structs hold service interfaces — never access DAOs directly from handlers
- Extract path params via `r.PathValue("id")` (stdlib `net/http.ServeMux`, Go 1.22+)
Validation functions close over the request struct and return `func() *errors.ServiceError`:

## Error Responses
```go
validateFuncs := []validate{
validateKind(&req, "Kind", "kind", expectedKind),
validateName(&req, "Name", "name", minLen, maxLen),
validateSpec(&req, "Spec", "spec"),
validateLabels(&req, "Labels"),
}
```

Errors are written via `handleError()` in `framework.go` which calls `err.AsProblemDetails()` for RFC 9457 format. The trace ID comes from `r.Header.Get("X-Request-Id")`.
Common validators (in `validation.go`):
- `validateKind(req, fieldName, jsonName, expectedKind)` — Ensures `kind` matches descriptor
- `validateName(req, fieldName, jsonName, minLen, maxLen)` — Name length constraints
- `validateSpec(req, fieldName, jsonName)` — Spec is non-nil
- `validateLabels(req, fieldName)` — Labels follow RFC 1123 rules
- `validatePatchRequest(req)` — At least one field is set
- `validateNotEmpty(req, fieldName, jsonName)` — String field is non-empty
- `validateMaxLength(req, fieldName, jsonName, maxLen)` — String length constraint

## Related CLAUDE.md Files
## Owner Verification

For nested routes, verify the child belongs to the parent:

```go
if err := h.checkOwnership(r, id); err != nil {
handleError(r, w, err)
return
}
```

This is a no-op for flat routes (no `parent_id` in path).

## Error Handling

Use `handleError(r, w, err)` to write RFC 9457 Problem Details responses:
- Logs errors with structured fields (trace ID, error code, HTTP status)
- Automatically extracts trace ID from request context
- Sets `instance` to the request path
- Error constructor functions (from `pkg/errors`)


## Path Parameters

Extract route parameters with r.PathValue

```go
id := r.PathValue("id")
// Extract parent_id
if parentID := r.PathValue("parent_id"); parentID != "" {
if _, svcErr := h.resourceService.GetByOwner(ctx, h.descriptor.Kind, id, parentID); svcErr != nil {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: h.resourceService is the field on ResourceStatusHandler, not ResourceHandler. This ownership example is a ResourceHandler pattern, where the field is h.service. Might be worth changing on the next PR.

handleError(r, w, svcErr)
return
}
}
```

## Field Filtering

Apply `?fields=` query parameter filtering:

```go
result, svcErr := applyFieldFilter(r, presenters.PresentResource(resource))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if err != nil checks an undefined variable, the assignment above is svcErr. Would fail to compile if copied verbatim.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

if svcErr != nil {
handleError(r, w, svcErr)
return
}

```

For list responses:
```go
if listArgs.Fields != nil {
filtered, svcErr := presenters.SliceFilter(listArgs.Fields, result)
if svcErr != nil {
handleError(r, w, svcErr)
return // REQUIRED — omitting this causes a double-write
}
writeJSONResponse(w, r, http.StatusOK, filtered)
return // REQUIRED — prevents falling through to the unfiltered write below
}
writeJSONResponse(w, r, http.StatusOK, result)
```

## Related Files

- `pkg/services/CLAUDE.md` — Service layer patterns
- `pkg/errors/CLAUDE.md` — Error constructors and codes
- `helpers.go` — `writeJSONResponse`,`decodeAndValidate`, `handleError`, `applyFieldFilter` implementations
- `validation.go` — Validator functions (`validateKind`, `validateName`, `validateSpec`, etc.)
Loading