การลงทะเบียนตัวให้บริการแบบไดนามิกด้วย DiscoveryService
สแกนและเชื่อมต่อตัวให้บริการขณะรันไทม์โดยใช้ DiscoveryService และ MetadataScanner สำหรับระบบปลั๊กอิน
การลงทะเบียนตัวให้บริการแบบไดนามิกด้วย DiscoveryService เป็นบทเรียน NestJS Enterprise Backend APIs ฟรีบน CoddyKit นี่คือบทเรียนที่ 2 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน NestJS Enterprise Backend APIs และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส NestJS Enterprise Backend APIs มีบทเรียนทั้งหมด 4 บทเรียน
บางส่วนของบทเรียนนี้ยังไม่ได้รับการแปล และแสดงเป็นภาษาอังกฤษ
The Plugin Wiring Problem
In a plugin architecture, you don't know at compile time which handlers, strategies, or adapters will exist. A hexagonal core defines ports; plugins supply adapters. The challenge: how does the framework find and wire those adapters without you hand-registering each one?
- Hard-coded arrays in a module are brittle — every new plugin edits core code.
- You want providers to self-declare their role via decorators, then be discovered at runtime.
NestJS ships @nestjs/core's DiscoveryService and MetadataScanner exactly for this. They let you scan the live DI container and react to metadata.
Marking Providers with a Decorator
The pattern starts with a custom decorator that stamps metadata onto a class. We use SetMetadata (or Reflector.createDecorator) so the scanner can later filter for it.
Here a @Plugin() decorator tags a class as a discoverable plugin and carries a name so the registry can key on it.
import { SetMetadata } from '@nestjs/common';
export const PLUGIN_KEY = 'app:plugin';
export interface PluginMeta {
name: string;
}
export const Plugin = (meta: PluginMeta): ClassDecorator =>
SetMetadata(PLUGIN_KEY, meta);
// A plugin author writes:
@Plugin({ name: 'csv-exporter' })
export class CsvExporter {
export(rows: unknown[]): string {
return rows.map((r) => JSON.stringify(r)).join('\n');
}
}What DiscoveryService Gives You
DiscoveryService is exported by the DiscoveryModule. Inject it and you get two key methods:
getProviders()— every provider instance wrapper in the application container.getControllers()— every controller wrapper.
Each item is an InstanceWrapper with .instance (the live object), .metatype (the class), and .name. You filter these wrappers by reading metadata off the metatype with a Reflector.
import { Module } from '@nestjs/common';
import { DiscoveryModule } from '@nestjs/core';
import { PluginRegistry } from './plugin.registry';
@Module({
imports: [DiscoveryModule], // exposes DiscoveryService + MetadataScanner
providers: [PluginRegistry],
exports: [PluginRegistry],
})
export class PluginCoreModule {}Scanning Providers on Bootstrap
Run discovery after the container is fully built. Implement OnModuleInit (or OnApplicationBootstrap if you need every module ready). Filter wrappers whose metatype carries your PLUGIN_KEY metadata.
Guard against null wrappers: some entries (value providers, request-scoped placeholders) have no metatype or no instance.
import { Injectable, OnModuleInit } from '@nestjs/common';
import { DiscoveryService, Reflector } from '@nestjs/core';
import { PLUGIN_KEY, PluginMeta } from './plugin.decorator';
@Injectable()
export class PluginRegistry implements OnModuleInit {
private readonly plugins = new Map<string, object>();
constructor(
private readonly discovery: DiscoveryService,
private readonly reflector: Reflector,
) {}
onModuleInit(): void {
for (const wrapper of this.discovery.getProviders()) {
const { instance, metatype } = wrapper;
if (!instance || !metatype) continue;
const meta = this.reflector.get<PluginMeta>(PLUGIN_KEY, metatype);
if (!meta) continue;
this.plugins.set(meta.name, instance);
}
}
get(name: string): object | undefined {
return this.plugins.get(name);
}
}MetadataScanner for Method-Level Hooks
Sometimes the plugin point isn't the class but a method — e.g. @EventHandler('order.created') on individual methods. MetadataScanner walks every method of an instance's prototype so you can read per-method metadata.
Use getAllMethodNames(prototype) (modern API) and inspect each handler with the Reflector.
import { Injectable, OnModuleInit } from '@nestjs/common';
import { DiscoveryService, MetadataScanner, Reflector } from '@nestjs/core';
export const EVENT_KEY = 'app:event';
@Injectable()
export class EventBinder implements OnModuleInit {
constructor(
private readonly discovery: DiscoveryService,
private readonly scanner: MetadataScanner,
private readonly reflector: Reflector,
) {}
onModuleInit(): void {
for (const w of this.discovery.getProviders()) {
if (!w.instance || !w.metatype) continue;
const proto = Object.getPrototypeOf(w.instance);
for (const method of this.scanner.getAllMethodNames(proto)) {
const event = this.reflector.get<string>(EVENT_KEY, proto[method]);
if (event) this.bind(event, w.instance, method);
}
}
}
private bind(event: string, target: object, method: string): void {
// register target[method] as a listener for `event`
}
}The Method-Level Decorator
Pair the binder with a method decorator. Note it's a MethodDecorator — SetMetadata attaches the value to the method's descriptor.value, which is exactly what reflector.get(EVENT_KEY, proto[method]) reads.
This keeps the wiring declarative: a plugin author adds an annotation and the core binds it — no manual emitter.on(...) calls.
import { SetMetadata } from '@nestjs/common';
import { EVENT_KEY } from './event.binder';
export const OnEvent = (event: string): MethodDecorator =>
SetMetadata(EVENT_KEY, event);
@Injectable()
export class InventoryPlugin {
@OnEvent('order.created')
reserveStock(payload: { orderId: string }): void {
// adjust stock for payload.orderId
}
@OnEvent('order.cancelled')
releaseStock(payload: { orderId: string }): void {
// restore stock
}
}Modeling Discovery in Plain TypeScript
Strip away NestJS and the core idea is simple: a registry maps a key to an instance discovered from a list, then routes calls by key. This standalone model captures the registry semantics you'll wire to DiscoveryService.
The exact same Map-based lookup powers the real registry — only the source of instances differs.
interface Exporter {
readonly name: string;
export(rows: object[]): string;
}
class CsvExporter implements Exporter {
name = 'csv';
export(rows: object[]): string {
return rows.map((r) => Object.values(r).join(',')).join('\n');
}
}
class JsonExporter implements Exporter {
name = 'json';
export(rows: object[]): string {
return JSON.stringify(rows);
}
}
class Registry {
private map = new Map<string, Exporter>();
register(...plugins: Exporter[]): void {
for (const p of plugins) this.map.set(p.name, p);
}
run(name: string, rows: object[]): string {
const p = this.map.get(name);
if (!p) throw new Error('Unknown exporter: ' + name);
return p.export(rows);
}
}
const reg = new Registry();
reg.register(new CsvExporter(), new JsonExporter());
const data = [{ id: 1, sku: 'A' }, { id: 2, sku: 'B' }];
console.log(reg.run('csv', data));
console.log(reg.run('json', data));Discovery Timing and Lifecycle
Timing matters. The DI container is only complete at certain lifecycle phases:
onModuleInit— fires per module after its providers resolve. Fine if all plugins live in one module.onApplicationBootstrap— fires once, after all modules initialized. Safest for cross-module plugin scanning.
Scanning too early yields an empty or partial provider list. Prefer onApplicationBootstrap when plugins can ship in feature modules loaded later.
import { Injectable, OnApplicationBootstrap } from '@nestjs/common';
import { DiscoveryService, Reflector } from '@nestjs/core';
import { PLUGIN_KEY, PluginMeta } from './plugin.decorator';
@Injectable()
export class PluginRegistry implements OnApplicationBootstrap {
private readonly plugins = new Map<string, object>();
constructor(
private readonly discovery: DiscoveryService,
private readonly reflector: Reflector,
) {}
onApplicationBootstrap(): void {
const found = this.discovery
.getProviders()
.filter((w) => w.instance && w.metatype)
.map((w) => ({
meta: this.reflector.get<PluginMeta>(PLUGIN_KEY, w.metatype!),
instance: w.instance,
}))
.filter((x) => x.meta);
for (const { meta, instance } of found) {
this.plugins.set(meta!.name, instance);
}
}
}Scope Pitfalls: REQUEST and TRANSIENT
Discovery sees singletons cleanly. Beware non-default scopes:
Scope.REQUEST/Scope.TRANSIENTproviders may havewrapper.instance === nullat bootstrap — there's no single instance to cache.- A discovered request-scoped instance would be stale and leak per-request state if you cache it.
Rule of thumb: keep plugins singleton-scoped. If a plugin truly needs request data, discover the class and resolve a fresh instance per request via ModuleRef.resolve() instead of caching the instance.
import { Injectable } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
@Injectable()
export class ScopedPluginInvoker {
constructor(private readonly moduleRef: ModuleRef) {}
// metatype was discovered earlier; resolve fresh per request
async invoke<T>(metatype: new (...a: any[]) => T): Promise<T> {
return this.moduleRef.resolve(metatype, undefined, { strict: false });
}
}Validating and Guarding the Registry
A plugin system that silently swallows duplicates or missing contracts is a debugging nightmare. Add guards during discovery:
- Duplicate names — throw, don't overwrite, so two plugins can't collide on one key.
- Contract check — verify the instance implements the expected method shape before trusting it.
Failing fast at bootstrap turns a runtime plugin bug into a clear startup error.
private register(name: string, instance: object): void {
if (this.plugins.has(name)) {
throw new Error(`Duplicate plugin name: ${name}`);
}
if (typeof (instance as { export?: unknown }).export !== 'function') {
throw new Error(`Plugin ${name} missing export()`);
}
this.plugins.set(name, instance);
}Why This Fits Hexagonal Design
Discovery-based registration is the runtime glue of ports and adapters:
- The core defines a port (an interface) and a registry keyed by capability.
- Each adapter/plugin declares itself with a decorator — it depends on the core's contract, never the reverse.
- Adding a capability means dropping in a new annotated provider; no edits to core wiring.
This inverts the dependency direction (the Dependency Inversion Principle) and keeps the core closed for modification but open for extension — the heart of plugin architecture.
Quick Check
You build a plugin registry that caches each discovered provider's .instance in a Map during onApplicationBootstrap. One plugin is declared with Scope.REQUEST. What goes wrong and what's the right fix?
Recap
You built a runtime plugin system on NestJS discovery primitives:
- Tag plugins with a metadata decorator (
SetMetadata+ aPLUGIN_KEY), class-level for whole plugins, method-level for handlers. - Scan the container with
DiscoveryService.getProviders(), filtering wrappers viaReflector; useMetadataScanner.getAllMethodNames()for per-method hooks. - Time it with
onApplicationBootstrapfor cross-module safety, and skip wrappers lackinginstance/metatype. - Guard against duplicate keys and contract violations; keep plugins singleton-scoped, resolving via
ModuleRefonly when request scope is genuinely needed.
The payoff: a hexagonal core that's closed for modification yet open to new annotated adapters — zero core edits per plugin.
เรียนรู้ TypeScript ด้วย AI tutor — ฟรี
เขียนและเรียกใช้โค้ดจริงในเบราว์เซอร์ของคุณ รับความช่วยเหลือทันทีจาก AI tutor 24/7 และเรียนรู้ต่อจากที่คุณหยุดบนเว็บหรือในแอป
- คอร์ส
- 20
- บทเรียน
- 76
คำถามที่พบบ่อย
บทเรียน “การลงทะเบียนตัวให้บริการแบบไดนามิกด้วย DiscoveryService” ฟรีหรือไม่
ใช่ — ข้อความเต็มของ “การลงทะเบียนตัวให้บริการแบบไดนามิกด้วย DiscoveryService” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส NestJS Enterprise Backend APIs ให้อัปเกรดเป็น CoddyKit PRO คอร์ส NestJS Enterprise Backend APIs มีบทเรียนทั้งหมด 4 บทเรียน
คุณจะเรียนรู้อะไรในบทเรียน “การลงทะเบียนตัวให้บริการแบบไดนามิกด้วย DiscoveryService”
สแกนและเชื่อมต่อตัวให้บริการขณะรันไทม์โดยใช้ DiscoveryService และ MetadataScanner สำหรับระบบปลั๊กอิน คุณปฏิบัติ NestJS Enterprise Backend APIs ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน
คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน NestJS Enterprise Backend APIs หรือไม่
ไม่จำเป็นต้องมีประสบการณ์มาก่อน NestJS Enterprise Backend APIs บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 2 จากทั้งหมด 4 บทเรียน
บทเรียน “การลงทะเบียนตัวให้บริการแบบไดนามิกด้วย DiscoveryService” ใช้เวลานานแค่ไหน
บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย
ฉันเขียนและรันโค้ดในบทเรียน NestJS Enterprise Backend APIs นี้ได้ไหม
ได้ บทเรียน NestJS Enterprise Backend APIs ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ
บทเรียนทั้งหมดในหลักสูตรนี้
- พอร์ตและอะแดปเตอร์เพื่อแยกโดเมน
- การลงทะเบียนตัวให้บริการแบบไดนามิกด้วย DiscoveryService
- โมดูลโหลดแบบขี้เกียจและสวิตช์ฟีเจอร์
- จุดขยายด้วย API อ้างอิงโมดูล