# How NestJS MetadataScanner Works: Prototype Introspection for Route Discovery

> Discover how NestJS MetadataScanner uses prototype introspection and memoization to efficiently scan class inheritance chains for route discovery at startup. Learn its core functionality.

- Repository: [nestjs/nest](https://github.com/nestjs/nest)
- Tags: internals
- Published: 2026-03-01

---

**The MetadataScanner is a lightweight prototype introspection utility in NestJS that traverses class inheritance chains to identify callable methods, filtering out constructors and accessors while memoizing results to optimize route discovery at application startup.**

NestJS relies on the `MetadataScanner` to dynamically discover controller methods during the bootstrap process. Located in [`packages/core/metadata-scanner.ts`](https://github.com/nestjs/nest/blob/main/packages/core/metadata-scanner.ts), this utility serves as the foundation for the framework's routing system by extracting method names that can then be inspected for HTTP method decorators like `@Get()` or `@Post()`.

## Core Implementation in metadata-scanner.ts

The `MetadataScanner` class implements a single public method, `getAllMethodNames()`, which accepts a prototype object and returns an array of method names as strings.

### Prototype Chain Traversal and Caching

The scanner employs a `do-while` loop to climb the prototype chain using `Reflect.getPrototypeOf()`, stopping before reaching `Object.prototype`. It utilizes a private `cachedScannedPrototypes` Map to store results for each prototype, ensuring subsequent scans return cached arrays in O(1) time.

```typescript
// packages/core/metadata-scanner.ts
export class MetadataScanner {
  private readonly cachedScannedPrototypes: Map<object, string[]> = new Map();

  public getAllMethodNames(prototype: object | null): string[] {
    if (!prototype) return [];

    if (this.cachedScannedPrototypes.has(prototype)) {
      return this.cachedScannedPrototypes.get(prototype)!;
    }

    const visitedNames = new Map<string, boolean>();
    const result: string[] = [];
    this.cachedScannedPrototypes.set(prototype, result);

    do {
      for (const property of Object.getOwnPropertyNames(prototype)) {
        if (visitedNames.has(property)) continue;
        visitedNames.set(property, true);

        const descriptor = Object.getOwnPropertyDescriptor(prototype, property);
        if (descriptor?.set || descriptor?.get || isConstructor(property) ||
            !isFunction(prototype[property as keyof typeof prototype])) {
          continue;
        }
        result.push(property);
      }
    } while ((prototype = Reflect.getPrototypeOf(prototype)) &&
             prototype !== Object.prototype);

    return result;
  }
}

```

### Filtering Logic for Callable Members

During traversal, the scanner applies strict filters to exclude properties that cannot serve as route handlers. It retrieves property descriptors via `Object.getOwnPropertyDescriptor()` and skips any member that meets these criteria:

- **Getters or setters** (`descriptor.get` or `descriptor.set`)
- **Constructor methods** (`isConstructor(property)`)
- **Non-function values** (`!isFunction(prototype[property])`)

This ensures only callable method names are returned, preventing the routing system from attempting to register properties or accessor methods as HTTP endpoints.

## Integration with the Routing Pipeline

The `MetadataScanner` operates as a dependency for higher-level explorers that transform method names into executable routes.

### PathsExplorer Consumption

The `PathsExplorer` class in [`packages/core/router/paths-explorer.ts`](https://github.com/nestjs/nest/blob/main/packages/core/router/paths-explorer.ts) receives a `MetadataScanner` instance through its constructor. Its `scanForPaths()` method invokes `getAllMethodNames()` on controller prototypes, then iterates through the results to extract NestJS-specific metadata using reflection keys like `PATH_METADATA` and `METHOD_METADATA`.

```typescript
// packages/core/router/paths-explorer.ts
export class PathsExplorer {
  constructor(private readonly metadataScanner: MetadataScanner) {}

  public scanForPaths(instance: Controller, prototype?: object): RouteDefinition[] {
    const instancePrototype = isUndefined(prototype)
      ? Object.getPrototypeOf(instance)
      : prototype;

    return this.metadataScanner
      .getAllMethodNames(instancePrototype)
      .reduce((acc, method) => {
        const route = this.exploreMethodMetadata(instance, instancePrototype, method);
        if (route) acc.push(route);
        return acc;
      }, [] as RouteDefinition[]);
  }
}

```

### RouterExplorer Registration

The resulting `RouteDefinition[]` array passes to `RouterExplorer` in [`packages/core/router/router-explorer.ts`](https://github.com/nestjs/nest/blob/main/packages/core/router/router-explorer.ts), which registers the actual HTTP handlers with the underlying platform adapter. This includes applying versioning constraints, host filters, and middleware guards defined by the controller decorators.

## Practical Code Examples

### Direct Scanner Usage

You can instantiate `MetadataScanner` directly to inspect any class prototype:

```typescript
import { MetadataScanner } from '@nestjs/core/metadata-scanner';

class SampleService {
  foo() {}
  get bar() { return 1; }
  baz() {}
}

const scanner = new MetadataScanner();
const methods = scanner.getAllMethodNames(SampleService.prototype);
// methods => ['foo', 'baz']

```

The output excludes the getter `bar` and the constructor, returning only callable methods.

### Inspecting Controller Routes

Use `PathsExplorer` to extract route definitions from a controller instance:

```typescript
import { Controller, Get } from '@nestjs/common';
import { PathsExplorer } from '@nestjs/core/router/paths-explorer';
import { MetadataScanner } from '@nestjs/core/metadata-scanner';

@Controller('cats')
class CatsController {
  @Get()
  findAll() {}

  @Get(':id')
  findOne() {}
}

const scanner = new MetadataScanner();
const explorer = new PathsExplorer(scanner);
const routes = explorer.scanForPaths(new CatsController());

// routes => [
//   { path: ['/cats'], requestMethod: RequestMethod.GET, ... },
//   { path: ['/cats/:id'], requestMethod: RequestMethod.GET, ... }
// ]

```

### Custom Metadata Discovery

The scanner supports custom decorator discovery for plugin development:

```typescript
import { MetadataScanner } from '@nestjs/core/metadata-scanner';
import 'reflect-metadata';

const MY_META = Symbol('my_meta');

function MyDecorator() {
  return (target: any, key: string) => {
    Reflect.defineMetadata(MY_META, true, target, key);
  };
}

class Demo {
  @MyDecorator()
  hello() {}

  world() {}
}

const scanner = new MetadataScanner();
const methods = scanner.getAllMethodNames(Demo.prototype);
const annotated = methods.filter(m => Reflect.getMetadata(MY_META, Demo.prototype, m));
console.log(annotated); // ['hello']

```

## Performance Characteristics

The memoization strategy in `cachedScannedPrototypes` ensures that each prototype is scanned exactly once per application lifecycle. This prevents redundant reflection operations as the dependency injection container resolves controller instances, making the bootstrap process efficient even for large applications with complex inheritance hierarchies.

## Summary

- **MetadataScanner** in [`packages/core/metadata-scanner.ts`](https://github.com/nestjs/nest/blob/main/packages/core/metadata-scanner.ts) walks class prototype chains to discover callable method names while filtering out constructors and accessors.
- It caches results in a `Map` to provide O(1) lookups for repeated scans of the same prototype.
- **PathsExplorer** consumes the scanner in [`packages/core/router/paths-explorer.ts`](https://github.com/nestjs/nest/blob/main/packages/core/router/paths-explorer.ts) to extract route metadata and produce `RouteDefinition` objects.
- The scanner supports custom metadata discovery, making it useful for plugin authors who need to introspect class methods beyond standard HTTP routing.

## Frequently Asked Questions

### What does MetadataScanner exclude from method discovery?

The scanner excludes the class constructor, any properties with getters or setters, and non-function values. It only returns property names that are callable methods, ensuring that the routing system does not attempt to invoke properties or accessor methods as HTTP handlers.

### Where is MetadataScanner used in the NestJS codebase?

Beyond HTTP routing in `PathsExplorer` and `RouterExplorer`, the scanner is also utilized by `GatewayMetadataExplorer` in the WebSockets package ([`packages/websockets/gateway-metadata-explorer.ts`](https://github.com/nestjs/nest/blob/main/packages/websockets/gateway-metadata-explorer.ts)) and within the testing utilities ([`packages/testing/testing-module.builder.ts`](https://github.com/nestjs/nest/blob/main/packages/testing/testing-module.builder.ts)) to discover lifecycle hooks and provider methods.

### Can I use MetadataScanner for custom decorators outside of HTTP controllers?

Yes. Since the scanner only requires a prototype object and returns method names as strings, you can use it with any class to identify methods decorated with custom metadata. Combine it with `Reflect.getMetadata()` to filter for your specific decorator keys, as shown in the custom discovery example above.

### How does the caching mechanism improve performance?

The `cachedScannedPrototypes` Map stores scanned method name arrays against their prototype objects. When the same prototype is scanned again—common during module initialization or testing—the method returns the cached array immediately instead of re-traversing the prototype chain and re-evaluating property descriptors.