Last active
January 9, 2025 02:34
-
-
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)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Module } from '@nestjs/common'; | |
import { ApiRoutesCommand } from './api-routes.command'; | |
@Module({ | |
providers: [ApiRoutesCommand], | |
}) | |
export class ApiRoutesModule {} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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