Simplify Koa and Egg Routing with Decorators: A Step-by-Step Guide
This article explains how to use JavaScript decorators to define routes for Koa and Egg frameworks, covering class and method decorators, Reflect metadata, automatic router generation, controller loading, and practical code examples for streamlined backend development.
Preface
Many developers using
Koaor
Eggencounter the tedious pattern of defining a route in a controller and then repeating the same definition in a separate router file. This article shows how to avoid that duplication by using decorators.
The same approach works for both
Koaand
Egg, allowing you to keep the original style while progressively enhancing it with decorator‑based routing.
Decorator Usage
Decorators are syntactic sugar: a regular function invoked with
@functionNameplaced before a class or method definition.
Class Decorator
<code>@testable
class MyTestableClass {
// ...
}
function testable(target) {
target.isTestable = true;
}
MyTestableClass.isTestable // true
// Code adapted from Ruan Yifeng’s ES6 tutorial</code>The
targetargument receives the class itself; if the decorator returns a value, it replaces the class.
<code>@decorator
class A {}
// equivalent to
class A {}
A = decorator(A) || A;
</code>Method Decorator
Method decorators resemble
Object.definePropertyand receive three arguments:
target: the prototype of the class, whose
constructorpoints to the class.
name: the name of the decorated property.
descriptor: the property descriptor, identical to the one used by
Object.defineProperty.
Descriptor details can be found in the MDN documentation.
<code>function foo(target, name, descriptor) {
// custom logic
}
</code>When a decorator needs parameters, create a higher‑order function that returns the actual decorator.
<code>function foo(url) {
return function (target, name, descriptor) {
console.log(url);
};
}
class Bar {
@foo('/test')
baz() {}
}
</code>Reflect
ES6 introduces the
ReflectAPI for object manipulation. To store routing metadata we use the
reflect-metadatalibrary, which provides:
<code>// set metadata
Reflect.defineMetadata(metadataKey, metadataValue, target);
// get metadata
let result = Reflect.getMetadata(metadataKey, target);
</code>Implement Decorator Routing
Implementation Idea
The goal is to generate a router configuration for
Koa(or
Egg) automatically. By attaching metadata (path, HTTP method) to each method via decorators, we can later register all routes in a single step.
<code>const app = new Koa();
const router = new Router();
router.get('/user/info', UserInfoController);
router.get('/user/list', UserListController);
router.post('/user/create', UserCreateController);
app.use(router.routes());
</code>Controller
Define a
Controllerdecorator to store a common base URL for a class.
<code>/**
* Controller decorator
* @param {string} [baseUrl=''] Class base path
*/
Controller(baseUrl = '') {
return (target) => {
Reflect.defineMetadata(BASE_URL, baseUrl, target);
};
}
</code>Base HTTP Methods
Because Koa‑router’s methods share the same signature, we create a generic factory that produces method‑specific decorators.
<code>/**
* Utility to generate method decorators
*/
createMethodDecorator(method) {
return (url) => {
return (target, name, decorator) => {
// store controller for later processing
this.controllerList.add(target);
Reflect.defineMetadata(METHOD, method, decorator.value);
Reflect.defineMetadata(METHOD_URL, url || name, decorator.value);
};
};
}
</code>Generate Router File
Iterate over stored controllers, read their metadata, and register each route on the router.
<code>/**
* Register routes on a Koa router
*/
registerRouter(router) {
for (const controller of this.controllerList) {
const controllerCtor = controller.constructor;
const baseUrl = Reflect.getMetadata(BASE_URL, controllerCtor) || '';
const allProps = Object.getOwnPropertyNames(controller);
for (const prop of allProps) {
const handle = controller[prop];
if (typeof handle !== 'function') continue;
const method = Reflect.getMetadata(METHOD, handle);
const url = Reflect.getMetadata(METHOD_URL, handle);
if (method && url && router[method]) {
const completeUrl = this.prefix + baseUrl + url;
router[method](completeUrl, handle);
}
}
}
}
</code>Load All Controllers
Automatically require every controller file using
require-context(or
require.contextin Webpack).
<code>import requireContext from 'require-context';
export const load = function (path) {
const ctx = requireContext(path, true, /\.js$/);
ctx.keys().forEach(key => ctx(key));
};
// usage: load(path.resolve(__dirname, './controller'));
</code>Extension
Beyond automatic route registration, decorators can handle authentication, middleware, dependency injection, validation, logging, and more.
Conclusion
We have built a decorator‑based routing system for Koa (and similarly for Egg or Express). By adapting the
registerRouterfunction, existing projects can gradually adopt decorator syntax without rewriting all routes.
The demo code is available at Koa-Decorator-Demo .
References
descriptor: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty#%E6%8F%8F%E8%BF%B0
reflect-metadata: https://rbuckton.github.io/reflect-metadata/
Koa-Decorator-Demo: https://github.com/xluos/Koa-Decorator-Demo
WeDoctor Frontend Technology
Official WeDoctor Group frontend public account, sharing original tech articles, events, job postings, and occasional daily updates from our tech team.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.