Skip to content
Open
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
12 changes: 11 additions & 1 deletion apps/landing/src/content/docs/docs/core-concepts/options.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ ShiftAPI uses a layered option system. Options configure the API, groups, and in
|------|---------------|----------|
| `Option` | API, Group, and Route | `WithError`, `WithMiddleware`, `WithResponseHeader` |
| `APIOption` | API only (`New`) | `WithInfo`, `WithBadRequestError`, `WithInternalServerError` |
| `RouteOption` | Route only (`Get`, `Post`, etc.) | `WithStatus`, `WithRouteInfo` |
| `RouteOption` | Route only (`Get`, `Post`, etc.) | `WithStatus`, `WithRouteInfo`, `WithHidden` |

`Option` implements all three level interfaces, so `WithError` and `WithMiddleware` work everywhere. Level-specific options like `WithStatus` are restricted to their level at compile time.

Expand Down Expand Up @@ -84,3 +84,13 @@ shiftapi.Get(admin, "/stats", getStats) // GET /api/v1/admin/stats
```

Options merge across all three levels: `New()` (API-wide) → `Group()` (group-level) → route registration (route-level).

## Hiding routes from the spec

Use `WithHidden()` to exclude a route from the generated OpenAPI schema (and therefore from any generated client). The route is still registered and serves requests normally — it just won't appear in the spec or docs.

```go
shiftapi.Get(api, "/internal/health", healthCheck, shiftapi.WithHidden())
```

This is useful for internal endpoints, health checks, or debug routes that shouldn't be part of the public API surface.
9 changes: 7 additions & 2 deletions doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,8 +211,13 @@
// [WithError], [WithMiddleware], and [WithResponseHeader] all return [Option].
//
// Some options are level-specific: [WithInfo] and [WithBadRequestError] only work
// with [New] ([APIOption]), while [WithStatus] and [WithRouteInfo] only work with
// route registration functions ([RouteOption]).
// with [New] ([APIOption]), while [WithStatus], [WithRouteInfo], and [WithHidden]
// only work with route registration functions ([RouteOption]).
//
// Use [WithHidden] to exclude a route from the generated OpenAPI schema (and any
// generated client) while still serving it normally:
//
// shiftapi.Get(api, "/internal/health", healthCheck, shiftapi.WithHidden())
//
// Use [ComposeOptions] to bundle multiple [Option] values into a reusable option:
//
Expand Down
6 changes: 4 additions & 2 deletions handlerFuncs.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,10 @@ func registerRoute[In, Resp any](
pathType = rawInType
}

if err := api.updateSchema(method, fullPath, pathType, queryType, headerType, bodyType, outType, hasRespHeader, noBody, hasForm, rawInType, cfg.info, cfg.status, allErrors, allStaticHeaders); err != nil {
panic(fmt.Sprintf("shiftapi: schema generation failed for %s %s: %v", method, fullPath, err))
if !cfg.hidden {
if err := api.updateSchema(method, fullPath, pathType, queryType, headerType, bodyType, outType, hasRespHeader, noBody, hasForm, rawInType, cfg.info, cfg.status, allErrors, allStaticHeaders); err != nil {
panic(fmt.Sprintf("shiftapi: schema generation failed for %s %s: %v", method, fullPath, err))
}
}

errLookup := buildErrorLookup(allErrors)
Expand Down
12 changes: 12 additions & 0 deletions handlerOptions.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ type routeConfig struct {
errors []errorEntry
middleware []func(http.Handler) http.Handler
staticRespHeaders []staticResponseHeader
hidden bool
}

func (c *routeConfig) addError(e errorEntry) {
Expand Down Expand Up @@ -52,6 +53,17 @@ func WithRouteInfo(info RouteInfo) routeOptionFunc {
}
}

// WithHidden excludes the route from the generated OpenAPI schema (and
// therefore from any generated client). The route is still registered and
// serves requests normally.
//
// shiftapi.Get(api, "/internal/health", healthCheck, shiftapi.WithHidden())
func WithHidden() routeOptionFunc {
return func(cfg *routeConfig) {
cfg.hidden = true
}
}

// WithStatus sets the success HTTP status code for the route (default: 200).
// Use this for routes that should return 201 Created, 204 No Content, etc.
func WithStatus(status int) routeOptionFunc {
Expand Down
31 changes: 31 additions & 0 deletions shiftapi_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,37 @@
}
}

func TestWithHiddenExcludesRouteFromSpec(t *testing.T) {
api := newTestAPI(t)
shiftapi.Get(api, "/health", func(r *http.Request, _ struct{}) (*Status, error) {
return &Status{OK: true}, nil
}, shiftapi.WithHidden())

spec := api.Spec()
if spec.Paths.Find("/health") != nil {
t.Fatal("hidden route should not appear in spec paths")
}
}

func TestWithHiddenRouteStillServes(t *testing.T) {
api := newTestAPI(t)
shiftapi.Get(api, "/health", func(r *http.Request, _ struct{}) (*Status, error) {
return &Status{OK: true}, nil
}, shiftapi.WithHidden())

srv := httptest.NewServer(api)
defer srv.Close()

resp, err := http.Get(srv.URL + "/health")
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()

Check failure on line 588 in shiftapi_test.go

View workflow job for this annotation

GitHub Actions / go

Error return value of `resp.Body.Close` is not checked (errcheck)
if resp.StatusCode != http.StatusOK {
t.Fatalf("expected 200, got %d", resp.StatusCode)
}
}

func TestSpecGetHasNoRequestBody(t *testing.T) {
api := newTestAPI(t)
shiftapi.Get(api, "/health", func(r *http.Request, _ struct{}) (*Status, error) {
Expand Down
Loading