aviskase

Home About Archive RSS

Automap API operation handlers in components-based project

published2020-10-25
updated2020-10-28
reading time2 mins
categoriesit

Node.js best practices repo is the most comprehensive list of style guides and architectural tips for Node.js apps I’ve seen. The very first of them is about structuring projects based on components instead of layers.

For example, the typical layers-based layout would be:

.
├── common
│   └── utils.ts
├── routers
│   ├── locations.router.ts
│   └── users.router.ts
├── services
│   ├── locations.service.ts
│   └── users.service.ts
└── index.ts

In comparison, this is a components-based layout:

.
├── common
│   └── utils.ts
├── components
│   ├── locations
│   │   ├── locations.router.ts
│   │   └── locations.service.ts
│   └── users
│       ├── users.router.ts
│       └── users.service.ts
└── index.ts

When it comes to API design, the design-first approach is the most recommended: write the OpenAPI description document (or in any other format), then write the implementation code. Usually, you’d want to leverage the description doc as much as possible to automate routine stuff, like request/response validation and operation handlers automapping.

For these purposes I use express-openapi-validator package. The default behavior for automapping is:

This behavior isn’t compatible with the components-base layout, since there is no such thing as base directory: each handler lives in the separate directory. Fortunately, you can customize default behavior with custom resolver!

For example, custom resolver I wrote:

operationHandlers: {
    basePath: path.join(__dirname, 'components'),
    resolver: (basePath: string, route, apiDoc) => {
        const pathKey = route.openApiRoute.substring(route.basePath.length);
        const schema = apiDoc.paths[pathKey][route.method.toLowerCase()];
        const functionName = schema['operationId'];
        const [componentName, routerName] = schema['x-eov-operation-handler'].split('.');
        const routerPath = routerName ? `${routerName}.router` : `${componentName}.router`;
        const modulePath = path.join(basePath, componentName, routerPath);
        const handler = require(modulePath);
        if (handler[functionName] === undefined) {
            throw new Error(`Could not find a [${functionName}] function in ${modulePath} when trying to route [${route.method} ${route.expressRoute}].`);
        }
        return handler[functionName];
    }
}

The code was updated after the fix #426. Should be a valid example as of version v4.3.6.

It assumes several things about the project’s structure:

Why so many assumptions? First, I wanted it simple. Second, I avoided introducing libraries for glob file search (yes, Node.js doesn’t have globs in standard library, bleh). And third, it will be fairly easy to change anyway!

older  · · ·  newer