I have a system of sharing types between the backend in frontend which looks something like this in the controller:
@Controller()
export class MyController {
@ApplyRoute('GET /foo')
async findAll(
@Query() { name }: Routes['GET /foo']['query'], // repeated route name
): Promise<Routes['GET /foo']['response']> { // repeated route name
return { foo: true }
}
}
Note how 'GET /foo' appears 3 times: once to declare the http method/path, once to get the type of query parameters, and once to enforce the return type.
I'm looking for a way to somehow remove that repetition, but I can't seem to think of any way that plays nice with the decorators. Right now someone could use the path of one route and then use a different route's query
or response
, which is probably quite wrong.
How can this duplication of route names be removed?
For example, something that does what this totally invalid syntax implies:
@Controller()
export class MyController {
@ApplyRoute('GET /foo') {
async findAll(
@Query() { name }: CurrentRoute['query'],
): Promise<CurrentRoute['response']> {
return { foo: true }
}
}
}
For additional info, the plumbing works like this.
There is a type that defines all routes, and the types that route accepts and returns:
type Routes = {
'GET /foo': {
query: { name: string }
response: { foo: true }
}
'GET /bar': {
query: {},
response: { bar: true }
}
}
Then we have a little helper decorator which parses the route name to the @RequestMapping()
decorator to set the http method and path for a controller method:
export function ApplyRoute(routeName: keyof Routes): MethodDecorator {
const [method, path] = routeName.split(' ') as ['GET' | 'POST', string]
return applyDecorators(
RequestMapping({ path, method: RequestMethod[method] }),
)
}
It's possible to use the dynamic routing techniques to remove the code duplication. Consider this example:
// define a controller that will host all methods of `Routes`
// it's, of course, also possible to have many of these or even have them generated based on the `Routes` descriptor
@Controller()
export class MyController {}
function Action<T extends keyof Routes>(route: T, fn: (query: Routes[T]["query"]) => Routes[T]["response"]): void {
const [method, path] = route.split(" ") as ["GET" | "POST", string];
const key = path.split("/").join("_");
// assigning the function body to a method in the controller class
MyController.prototype[key] = fn;
// applying the `Query` directive to the first parameter
// this could also be configured through the `Routes` in case if you have e.g. post methods
Query()(MyController.prototype, key, 0);
// applying the `Get` decorator to the controller method
RequestMapping({ path: path, method: RequestMethod[method] })(
MyController,
key,
Object.getOwnPropertyDescriptor(MyController.prototype, key)
);
}
// now, register the methods in the global scope
Action("GET /foo", ({ name: string }) => ({ foo: true }));
Action("GET /bar", () => ({ bar: true }));