CodexaCodexa

Inspection & Tags

Query the routes, plugins, and services an app actually has installed, and toggle groups of routes on or off by tag, even after boot.

app.inspect() reads back everything installed in the app: routes, middleware, plugins, and exposed services. Reach for it to build an internal /admin/routes page, print a route map on startup for a sanity check, or assert in a test that a plugin exposed the service another plugin expects. It is not meant for anything a request handler needs on the hot path, since it walks the whole route table on every call.

const result = app.inspect();

console.log(result.summary);
// { routeCount: 12, enabledRouteCount: 11, disabledRouteCount: 1, pluginCount: 3, ... }

console.table(result.routes);

A worked example: an admin routes endpoint

A common use of inspect() is exposing a small internal endpoint that lists every route the app actually has, useful for a status page or for verifying a deploy registered what you expected.

A plugin's setup does not receive the app instance on its own, only scope. Pass app in through the plugin's config instead, the same way Resource Composition covers for any other shared value.

import { definePlugin, type ICodexaHttp } from '@codexa/core/http';

interface AdminConfig {
  readonly app: ICodexaHttp;
}

export const adminPlugin = definePlugin<'admin', AdminConfig>({
  name: 'admin',
  setup(scope, config) {
    scope.route({
      method: 'GET',
      path: '/admin/routes',
      handler: (ctx) => {
        const { routes } = config.app.inspect({ includeDisabled: false });
        return ctx.json(
          routes.map((route) => ({
            method: route.method,
            path: route.path,
            plugin: route.pluginName,
          })),
        );
      },
      options: { name: 'admin.routes', tags: ['admin'] },
    });
  },
});

const app = createApp();
app.install(adminPlugin, { app });

Filtering a query

Every field on InspectQuery is optional, and combining them narrows the result to routes, middleware, and plugins matching all of the fields you set.

app.inspect({
  plugins: ['orders'],
  tags: ['orders:create'],
  methods: ['POST'],
  includeDisabled: true,
});
interface InspectQuery {
  tags?: readonly string[];
  plugins?: readonly string[];
  routes?: readonly string[];
  services?: readonly string[];
  methods?: readonly HttpMethod[];
  versions?: readonly string[];
  includeDisabled?: boolean;
}
  • tags, plugins, routes, and methods narrow which routes come back, and services narrows which exposed services come back, matched against the service name a plugin passed to exposeService.
  • versions filters to routes registered under a specific version, leaving unversioned routes out unless you also ask for them by other fields.
  • includeDisabled defaults to true. A query with no includeDisabled set returns disabled routes right alongside enabled ones, each with its own enabled: false. Pass includeDisabled: false explicitly to see only what a live request could actually reach.

tags here is an exact match against each route's options.tags, not the *prefix* pattern language appliedOn uses for middleware. Querying tags: ['orders'] only matches routes tagged exactly orders, not orders:create.

Reading the result

interface InspectResult {
  query?: InspectQuery;
  summary: {
    routeCount: number;
    enabledRouteCount: number;
    disabledRouteCount: number;
    pluginCount: number;
    serviceCount: number;
    middlewareCount: number;
  };
  routes: readonly InspectRoute[];
  middlewares: readonly InspectMiddleware[];
  plugins: readonly InspectPlugin[];
  services: readonly InspectService[];
}

Four kinds of records make up a result, and each answers a different question.

RecordAnswersKey fields
InspectRouteWhat can a request reach, and through what?name, method, path, enabled, tags, pluginName, version, versionHeader, openapi, and the list of middleware that applies to it
InspectMiddlewareWhat runs before a route's handler?name, kind ('plugin' or 'inline'), enabled, tags, appliedOn, priority, pluginName
InspectPluginWhat did a plugin register, and what does it depend on?name, metadata, dependsOn, services, routeCount, middlewareCount
InspectServiceDoes a service actually exist?name, pluginName, exists

middlewares, plugins, and services only come back populated when the query asks for them. Calling app.inspect() with no arguments returns routes filled in and the other three empty. plugins and middlewares need tags or plugins set in the query, services needs services set. For the full list of installed plugin names with no filtering at all, use app.installedPlugins() instead.

InspectPlugin's dependsOn and services together are enough to draw a dependency graph for the plugins you ask about.

const { plugins } = app.inspect({ plugins: ['orders', 'payments', 'notifications'] });
for (const plugin of plugins) {
  console.log(plugin.name, '->', plugin.dependsOn.join(', ') || '(no dependencies)');
}

Toggling routes by tag

enableByTags and disableByTags turn groups of routes and plugin middleware on or off at once, matched the same exact way inspect()'s tags filter is, not through the appliedOn pattern language.

app.disableByTags('beta');
// every route or middleware tagged exactly "beta" stops matching requests

app.enableByTags('beta');
// and matches again

Unlike most registration methods, these can be called after boot(), which makes them suitable for a feature flag toggled from a running admin route rather than only from startup configuration. Extending the adminPlugin above with the same app passed through its config:

scope.route({
  method: 'POST',
  path: '/admin/features/:tag/disable',
  handler: (ctx) => {
    config.app.disableByTags(ctx.params.tag);
    return ctx.json({ disabled: ctx.params.tag });
  },
  options: { name: 'admin.features.disable', tags: ['admin'] },
});

A disabled route behaves exactly like one that does not exist. dispatch calls your onNotFound handler for it, the same as an unmatched path.

Checking one route directly

app.hasRoute('GET', '/orders/:id');       // true or false
app.toRegExp('GET', '/orders/:id');       // the compiled matching RegExp, or null

toRegExp is useful when something outside the app, a reverse proxy or a custom static file rule, needs to know whether a path would be claimed by a registered route.

On this page