Skip to content

Instantly share code, notes, and snippets.

@mp035
Last active January 9, 2025 02:34
Show Gist options
  • Save mp035/3a5eece578b8e5d94e01548ef61f9c13 to your computer and use it in GitHub Desktop.
Save mp035/3a5eece578b8e5d94e01548ef61f9c13 to your computer and use it in GitHub Desktop.
A nestjs-command module to extract all routes from a nest.js application (intended for managing the api specification on the client, but could be used elsewhere)
import { Module } from '@nestjs/common';
import { ApiRoutesCommand } from './api-routes.command';
@Module({
providers: [ApiRoutesCommand],
})
export class ApiRoutesModule {}
import { Injectable } from '@nestjs/common';
import { Command } from 'nestjs-command';
import * as fs from 'fs';
import * as glob from 'glob';
import * as path from 'path';
import * as ts from 'typescript';
@Injectable()
export class ApiRoutesCommand {
@Command({
command: 'api-routes:generate',
describe: 'Generate a list of API routes for the frontend',
})
async generateApiRoutes() {
// grab each controller file from ../server/src/**/*.controller.ts
const fileList = glob.sync(path.join(__dirname, '../**/*.controller.ts'));
function findNodesByType(node, type) {
const nodes = [];
function findNodes(node) {
if (node.kind === type) {
nodes.push(node);
}
ts.forEachChild(node, findNodes);
}
findNodes(node);
return nodes;
}
const apiRoutes = {};
// iterate over the file list
for (const file of fileList) {
// parse the file
const program = ts.createProgram([file], { allowJs: true });
const source = program.getSourceFile(file);
const classes = findNodesByType(source, ts.SyntaxKind.ClassDeclaration);
for (const classObj of classes) {
// check if the class has a 'Controller' decorator, if not, continue, otherwise, get the first parameter as the route
const classDecoratorNodes = findNodesByType(
classObj,
ts.SyntaxKind.Decorator,
);
const controllerDecorator = classDecoratorNodes.find(
(decorator) =>
decorator.expression.expression.escapedText === 'Controller',
);
if (!controllerDecorator) {
continue;
}
const controllerRoute = controllerDecorator.expression.arguments.length
? controllerDecorator.expression.arguments[0].text
: '';
const className = classObj.name.escapedText;
apiRoutes[className] = {};
const apiController = apiRoutes[className];
// find the public methods of the class with decorators
const methods = findNodesByType(
classObj,
ts.SyntaxKind.MethodDeclaration,
);
for (const methodNode of methods) {
const methodName = methodNode.name.escapedText;
// find the decorators
const methodDecoratorNodes = findNodesByType(
methodNode,
ts.SyntaxKind.Decorator,
);
const methodDecorators = [];
for (const decorator of methodDecoratorNodes) {
// get the decorator name and first parameter (if any)
methodDecorators.push({
name: decorator.expression.expression.escapedText,
value: decorator.expression.arguments.length
? decorator.expression.arguments[0].text
: '',
});
}
// if the decorator names do not contain at least one of 'Get', 'Post', 'Put', 'Delete', 'Patch', 'Options', 'Head' or 'All' then continue to the next method
const validDecorators = [
'Get',
'Post',
'Put',
'Delete',
'Patch',
'Options',
'Head',
'All',
];
const routeDecorator = methodDecorators.find((decorator) =>
validDecorators.includes(decorator.name),
);
if (!routeDecorator) {
continue;
}
// if the method has a decorator, convert it to a http VERB
const method = routeDecorator.name.toUpperCase();
const path = routeDecorator.value;
/*
console.log(
`${className}.${methodName} : ${method} ${controllerRoute}/${path}`,
);
*/
process.stdout.write('#');
apiController[methodName] = {
method,
path: path ? `${controllerRoute}/${path}` : controllerRoute,
};
}
}
}
//generate the source code for the api-routes file
let sourceFileContent = `//THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
//To update this file, run 'npx nestjs-command api-routes:generate'
//from the server project directory.
type ApiRoute = {
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
path: string;
};
`;
for (const controller in apiRoutes) {
sourceFileContent += `class ${controller}Class {
`;
for (const method in apiRoutes[controller]) {
sourceFileContent += `public ${method}: ApiRoute = ${JSON.stringify(
apiRoutes[controller][method],
)};
`;
}
sourceFileContent += `}
`;
}
for (const controller in apiRoutes) {
sourceFileContent += `export const ${controller} = new ${controller}Class();
`;
}
// find and overwrite the src/lib/api-routes.js file
throw new Error("You have to adjust the output path below to ensure the route list is generated where you expect.");
const apiRoutesPath = path.join(
__dirname,
'../../../app/src/lib/api-routes.ts',
);
fs.writeFileSync(apiRoutesPath, sourceFileContent);
console.log('');
console.log(`apiRoutes written to ${apiRoutesPath}`);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment