PC Slower Than It Used to Be?
A free scan shows the junk files, broken settings and background clutter dragging Windows down - then fixes them in one click.Free scan · Windows 10 & 11Outdated Drivers Are Slowing You Down
One free scan finds every outdated or missing driver and matches the right update for your exact hardware.Free scan · exact hardware matchA metadata-driven Node.js framework lets you declare controllers and routes once, then inspect those declarations at startup to register handlers with an HTTP server. The useful learning goal is a small, understandable framework core—not a production-ready replacement for an established framework. This tutorial sketches that core in TypeScript and makes the lifecycle, metadata rules, and failure cases explicit.
What metadata-driven routing changes
In a hand-wired server, route declarations and application setup tend to accumulate together:
As an Amazon Associate I earn from qualifying purchases.
server.get("/users", userController.list.bind(userController));
server.post("/users", userController.create.bind(userController));
server.get("/users/:id", userController.get.bind(userController));
That is direct and easy to trace, but repeated registration can become tedious as the application grows. A metadata-driven design moves the route facts next to the controller methods. The framework later reads those facts and builds the route map.
Quick wins for a faster PC:
Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Repair Windows errors before they cause bigger problemsFix Now →Metadata is configuration, not magic: a decorator or registration function records information, and a bootstrap phase interprets it. NestJS documents attaching custom metadata to classes or handlers and retrieving it later through a reflection API; its documentation also shows that class-level and handler-level metadata need deliberate override or merge rules (NestJS execution context).
#1 Best Overall
Define a small, explicit metadata contract
Start with only what routing needs: a controller base path and a list of method/path/handler declarations. Keep the contract explicit so it can be validated without relying on inferred parameter types.
type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
type RouteDefinition = {
method: HttpMethod;
path: string;
propertyKey: string | symbol;
};
type ControllerDefinition = {
basePath: string;
routes: RouteDefinition[];
};
The framework needs a registry that associates a controller constructor with its definition. A JavaScript WeakMap is a suitable in-memory store because the constructor itself is the lookup key:
const controllers = new Set<Function>();
const definitions = new WeakMap<Function, ControllerDefinition>();
function definitionFor(target: Function): ControllerDefinition {
let definition = definitions.get(target);
if (!definition) {
definition = { basePath: "", routes: [] };
definitions.set(target, definition);
}
return definition;
}
This example uses explicit metadata fields; the framework does not need TypeScript to infer request validation rules from method signatures.
Rank #2
Record controller and route declarations
With TypeScript’s legacy decorator model, a class decorator can register the controller and its base path, while a method decorator records route details. The method decorator receives the prototype and method key, so it can add a declaration without instantiating the controller.
function Controller(basePath: string): ClassDecorator {
return (target) => {
const definition = definitionFor(target);
definition.basePath = basePath;
controllers.add(target);
};
}
function Route(method: HttpMethod, path: string): MethodDecorator {
return (target, propertyKey) => {
const constructor = target.constructor;
const definition = definitionFor(constructor);
definition.routes.push({ method, path, propertyKey });
};
}
const Get = (path: string) => Route("GET", path);
const Post = (path: string) => Route("POST", path);
@Controller("/users")
class UserController {
@Get("")
list() {
return [];
}
@Get("/:id")
get() {
return { id: "example" };
}
@Post("")
create() {
return { created: true };
}
}
Decorator syntax is only one expression of the contract. A framework could instead expose explicit calls that register the same controller and route records. That alternative can be useful when JavaScript support or simpler tooling matters more than declarative syntax.
Configure TypeScript deliberately
The TypeScript Handbook describes decorator support using the legacy decorator model as experimental. Its examples use experimentalDecorators; design-type metadata emission additionally uses emitDecoratorMetadata and imports reflect-metadata. The handbook cautions that this metadata mechanism is not part of the ECMAScript standard and may change (TypeScript Handbook: Decorators).
Rank #3
// tsconfig.json (relevant options)
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
For this router, the registry records its own route metadata, so it does not need emitted design types at all. Avoid enabling type metadata unless another feature actually consumes it. If you adopt that feature, document the expected compiler options, module setup, and runtime import as part of the framework’s supported toolchain. Do not assume decorator syntax or emitted metadata behaves identically across TypeScript configurations.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Bootstrap by resolving and binding routes
The bootstrap phase should instantiate each registered controller, combine its base path with each route path, resolve the method, and register a bound handler with a server adapter. NestJS documentation likewise presents application setup as more than decorators alone: an application needs an adapter and supporting setup (NestJS application setup documentation).
type ServerAdapter = {
register(method: HttpMethod, path: string, handler: (...args: any[]) => any): void;
};
function joinPaths(base: string, route: string): string {
const combined = `/${base}/${route}`.replace(//+/g, "/");
return combined.length > 1 ? combined.replace(//$/, "") : "/";
}
function bootstrap(adapter: ServerAdapter): void {
const registered = new Set<string>();
for (const ControllerType of controllers) {
const definition = definitions.get(ControllerType);
if (!definition) throw new Error(`Missing controller metadata: ${ControllerType.name}`);
if (!definition.basePath.startsWith("/")) {
throw new Error(`Controller path must start with '/': ${ControllerType.name}`);
}
const instance = new (ControllerType as new () => object)();
for (const route of definition.routes) {
const key = route.propertyKey;
const candidate = (instance as any)[key];
if (typeof candidate !== "function") {
throw new Error(`Route handler is not a method: ${ControllerType.name}.${String(key)}`);
}
if (!route.path.startsWith("/") && route.path !== "") {
throw new Error(`Route path must start with '/': ${String(key)}`);
}
const path = joinPaths(definition.basePath, route.path);
const identity = `${route.method} ${path}`;
if (registered.has(identity)) throw new Error(`Duplicate route: ${identity}`);
registered.add(identity);
adapter.register(route.method, path, candidate.bind(instance));
}
}
}
The adapter is intentionally abstract: a real implementation must translate the resolved method and path into the API of the chosen HTTP server. This separation keeps declaration discovery independent from the transport, but it does not implement request parsing, response formatting, error handling, or shutdown.
Rank #4
Make invalid declarations fail at startup
Startup is the right time to catch configuration mistakes before the server accepts traffic. The sketch rejects missing metadata, invalid path forms, non-method handlers, and duplicate method/path pairs. A usable implementation should also define how it handles these cases:
- Path normalization: Decide whether trailing slashes are equivalent and whether parameter syntax is validated by the adapter or framework.
- Duplicate semantics: Decide whether two declarations for the same method and resolved path are always an error. Failing clearly is safer than silently letting registration order decide.
- Controller inheritance: Specify whether subclasses inherit route metadata, replace it, or merge it. If metadata can come from both a base class and an overriding method, define which declaration wins.
- Construction and dependencies: The example assumes a zero-argument controller constructor. Constructor injection requires a separate dependency-resolution mechanism.
- Failure reporting: Include the controller, method, and conflicting route in errors so the owner can correct the declaration.
NestJS’s metadata documentation demonstrates that overriding one metadata value and merging class- and handler-level values are distinct policies, not automatic consequences of using decorators (NestJS reflection and metadata).
What the design gains—and what it costs
| Concern | Handwritten registration | Metadata-driven registration |
|---|---|---|
| Route visibility | Routes appear in the registration code. | Route declarations live near handlers; the final map depends on bootstrap resolution. |
| Flexibility | Registration logic is directly editable and explicit. | Conventions simplify declarations, while framework rules govern resolution. |
| Startup checks | Checks can be added where routes are registered. | A central discovery phase can validate all declarations before serving traffic. |
| Runtime assumptions | Can use plain JavaScript and the chosen server API. | Custom metadata can be explicit; emitted TypeScript design metadata adds compiler and runtime assumptions. |
| Operational scope | Application code supplies the surrounding lifecycle and policies. | The framework must still supply or specify errors, testing, lifecycle, and shutdown behavior. |
No performance or productivity advantage follows automatically from either structure. Those outcomes depend on the implementation and workload; the cited documentation describes patterns and setup, not comparative measurements.
Build a learning framework or adopt an established one?
A small custom framework is useful when the objective is to understand registration, metadata, bootstrap, and adapter boundaries—or when a deliberately narrow application needs a tailored convention. It also leaves you responsible for every policy and operational feature not included in the core.
For an application that needs a wider set of server-side conventions and infrastructure, evaluate an established framework such as NestJS. Its current documentation describes an application architecture and setup process; its execution-context guide illustrates metadata access and resolution policies (NestJS setup; NestJS execution context). A custom tutorial core is a learning exercise, not evidence that recreating those surrounding capabilities is inexpensive.
Other ecosystem projects demonstrate declarative routing as a pattern, including Resty.js, whose README shows a decorated controller registered with an application instance (Resty.js project README). That example establishes the pattern exists; it is not evidence of project maturity, performance, or production suitability. StreetJS also describes decorator-driven controllers, but version-specific compatibility should be checked against its current project documentation rather than inferred from an older listing (StreetJS documentation).
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




