Skip to content

Commit fe5f6a9

Browse files
Migrate API to typed Huma routes (#599)
The API migration needed to stop carrying two parallel contracts: typed Huma handlers on the server and hand-assembled frontend request paths in the UI. That split made enums, request bodies, response shapes, and error handling easy to drift, and review feedback showed the first migration pass still left some runtime parity and generated-client boundary gaps. This PR moves the server API onto grouped typed Huma route registration, exposes OpenAPI 3.1 for introspection, generates the frontend TypeScript client from that contract, and routes normal JSON frontend calls through generated services. Streaming, import, and download paths stay explicit where the generated JSON abstraction is not appropriate. It also addresses review follow-ups around route-group ownership, generated-code metadata, import response content types, secret-scan SSE parity, and generated-client error normalization. Validation performed locally: - go test -count=1 ./... - npm run generate:api - npm run check - npm test -- --run - git diff --check Co-authored-by: Marius van Niekerk <mariusvniekerk@users.noreply.github.com>
1 parent 1964e4f commit fe5f6a9

258 files changed

Lines changed: 12502 additions & 5954 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitattributes

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# OpenAPI TypeScript client generated by frontend/scripts/generate-api-client.mjs.
2+
frontend/src/lib/api/generated/** linguist-generated=true

.kata.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
version = 1
2+
3+
[project]
4+
name = "agentsview"

cmd/agentsview/cli.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/spf13/cobra"
1111
"go.kenn.io/agentsview/internal/config"
1212
"go.kenn.io/agentsview/internal/db"
13+
"go.kenn.io/agentsview/internal/server"
1314
"golang.org/x/term"
1415
)
1516

@@ -69,6 +70,7 @@ func newRootCommand() *cobra.Command {
6970
root.AddCommand(newClassifierCommand())
7071
root.AddCommand(newSecretsCommand())
7172
root.AddCommand(newVersionCommand())
73+
root.AddCommand(newOpenAPICommand())
7274

7375
defaultHelp := root.HelpFunc()
7476
root.SetHelpFunc(func(cmd *cobra.Command, args []string) {
@@ -97,6 +99,28 @@ func newServeCommand() *cobra.Command {
9799
return cmd
98100
}
99101

102+
func newOpenAPICommand() *cobra.Command {
103+
return &cobra.Command{
104+
Use: "openapi",
105+
Short: "Print OpenAPI 3.1 schema",
106+
GroupID: groupMeta,
107+
SilenceUsage: true,
108+
Args: cobra.NoArgs,
109+
RunE: func(cmd *cobra.Command, args []string) error {
110+
spec, err := server.OpenAPIJSON(server.VersionInfo{
111+
Version: version,
112+
Commit: commit,
113+
BuildDate: buildDate,
114+
})
115+
if err != nil {
116+
return err
117+
}
118+
_, err = cmd.OutOrStdout().Write(append(spec, '\n'))
119+
return err
120+
},
121+
}
122+
}
123+
100124
func newSyncCommand() *cobra.Command {
101125
var cfg SyncConfig
102126
cmd := &cobra.Command{

cmd/agentsview/cli_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package main
22

33
import (
44
"bytes"
5+
"encoding/json"
56
"os"
67
"testing"
78

@@ -52,6 +53,22 @@ func TestRootHelpShowsKeySectionsAndCommands(t *testing.T) {
5253
}
5354
}
5455

56+
func TestOpenAPICommandEmitsSpec(t *testing.T) {
57+
out, err := executeCommand(newRootCommand(), "openapi")
58+
require.NoError(t, err, "Execute")
59+
60+
var spec struct {
61+
OpenAPI string `json:"openapi"`
62+
Paths map[string]map[string]any `json:"paths"`
63+
}
64+
require.NoError(t, json.Unmarshal([]byte(out), &spec))
65+
assert.Equal(t, "3.1.0", spec.OpenAPI)
66+
require.Contains(t, spec.Paths, "/api/v1/sessions")
67+
assert.Contains(t, spec.Paths["/api/v1/sessions"], "get")
68+
require.Contains(t, spec.Paths, "/api/v1/sessions/{id}/rename")
69+
assert.Contains(t, spec.Paths["/api/v1/sessions/{id}/rename"], "patch")
70+
}
71+
5572
func TestRootNoArgsShowsHelp(t *testing.T) {
5673
out, err := executeCommand(newRootCommand())
5774
require.NoError(t, err, "Execute")

docs/huma-api-routes.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Huma API Routes
2+
3+
The server API is registered with Huma route groups. Keep each route group
4+
self-contained so OpenAPI ownership stays close to runtime behavior.
5+
6+
## File Ownership
7+
8+
- Put route registration, endpoint-specific input types, response wrapper
9+
types, enums, and handlers in the route group file that owns the path.
10+
- Keep `internal/server/huma_routes.go` limited to shared Huma plumbing:
11+
API configuration, route registration helpers, common path/query inputs,
12+
error conversion, schema naming, SSE/write helpers, and middleware.
13+
- Move a helper into shared plumbing only when at least two route groups use
14+
it and it has no domain-specific policy.
15+
- Do not add new typed handlers to a catch-all API file. Add a new group file
16+
when a new API area does not fit an existing group.
17+
18+
## Compatibility Guardrails
19+
20+
When changing route registration or generated client contracts:
21+
22+
- Preserve existing paths, methods, status codes, response events, and content
23+
types unless the change is intentional and covered by tests.
24+
- Add or update parity tests for JSON bodies, raw downloads, multipart imports,
25+
SSE terminal events, and error responses touched by the change.
26+
- Run `npm run generate:api` from `frontend/` and verify generated output only
27+
changes when the OpenAPI contract intentionally changed.
28+
- Keep generated frontend code under `frontend/src/lib/api/generated/`; it is
29+
marked as generated in `.gitattributes` and should not be hand-edited.

0 commit comments

Comments
 (0)