Skip to content

Hiding Routes

Not every endpoint belongs in your public API documentation. Internal tools, debug endpoints, or work-in-progress features can be hidden from the generated spec while still functioning normally.

Common reasons to exclude routes from the OpenAPI spec:

  • Internal endpoints used by your infrastructure (health checks, metrics).
  • Debug or admin tools that are not part of the public API surface.
  • Work-in-progress features that are deployed but not ready for consumers.

Hidden routes still handle requests. They just do not appear in the JSON spec or the docs UI.

Set hideFromDocs: true in the @Controller options to hide every route on that controller:

@Controller('/api/internal', { hideFromDocs: true })
export class InternalController implements IController {
@Route({
response: z.object({ status: z.string() }),
summary: 'Health check',
})
index(ctx: RouterContext) {
return ctx.json({ status: 'ok' })
}
@Route({
params: z.object({ id: z.string() }),
response: z.object({ data: z.unknown() }),
summary: 'Debug info',
})
show(ctx: RouterContext) {
return ctx.json({ data: {} })
}
}

Both GET /api/internal and GET /api/internal/:id work at runtime but are excluded from the spec.

Set hideFromDocs: true on a specific @Route to hide just that endpoint:

@Controller('/api/users', { tags: ['Users'] })
export class UsersController implements IController {
@Route({
response: userListSchema,
summary: 'List all users',
})
index(ctx: RouterContext) {
return ctx.json({ data: [] })
}
@Route({
params: z.object({ id: z.string() }),
response: z.object({ debug: z.unknown() }),
summary: 'Debug user state',
hideFromDocs: true,
})
show(ctx: RouterContext) {
// Hidden from docs, but still accessible
return ctx.json({ debug: {} })
}
}

Here GET /api/users appears in the docs, but GET /api/users/:id does not.

The hideFromDocs property also works in HTTP method decorator config:

@Get('/debug', {
response: z.object({ debug: z.unknown() }),
hideFromDocs: true,
})
debugInfo(ctx: RouterContext) {
return ctx.json({ debug: {} })
}

Routes registered with the @All decorator are automatically excluded from the OpenAPI specification. Since @All matches every HTTP method, including it would create ambiguous documentation.

import { Controller, All, RouterContext } from 'stratal/router'
@Controller('/api/proxy')
export class ProxyController {
@All('/:path{.+}', {
response: z.object({ proxied: z.boolean() }),
})
proxyAll(ctx: RouterContext) {
return ctx.json({ proxied: true })
}
}

Route-level hideFromDocs takes precedence over the controller-level setting. If a controller has hideFromDocs: true and a route does not explicitly set hideFromDocs, the route inherits the controller’s value and stays hidden.

For more dynamic control, use the OpenAPIConfigService to apply a custom route filter at runtime. This is useful when you want to filter routes based on request context, such as environment or user role.

Inject the config service in a middleware and call override() with a routeFilter function:

import { inject } from 'stratal/di'
import { Middleware, MiddlewareHandler } from 'stratal/middleware'
import { RouterContext } from 'stratal/router'
import { OPENAPI_TOKENS } from 'stratal/openapi'
import type { IOpenAPIConfigService } from 'stratal/openapi'
@Middleware()
export class DocsFilterMiddleware implements MiddlewareHandler {
constructor(
@inject(OPENAPI_TOKENS.ConfigService)
private openApiConfig: IOpenAPIConfigService,
) {}
async handle(ctx: RouterContext, next: () => Promise<void>) {
this.openApiConfig.override({
routeFilter: (path, pathItem) => {
// Only include /api/v1 routes in the spec
return path.startsWith('/api/v1')
},
})
await next()
}
}

The routeFilter function receives the path string and the OpenAPI path item object. Return true to include the route, false to exclude it. This filter runs in addition to hideFromDocs. Routes hidden by the flag are excluded before the filter runs.

See Interactive Docs UI to learn about the interactive documentation viewer and how to customize it.