koa-router schema validation and swagger api doc generation

lib: koa-router-ajv-swaggergen (woah such a big name)

Koa is a webframework very similar to expressjs. Its not a complete webframework rather a middleware framework as it allows writing middlewares easily in async way. Now that means you need a lot of plumbing to make it useful. But either way you will start off by selecting a router module of which there are many. Very soon you will realize that you need to write input validations, and eventually in a distributed team some sort of document to maintain apis and there are many more of this libs. Well granted you will need many more things to get going like authentication authorization, perhaps distributed logging, security. These are all independent libs that may require pick and choose approach. If you want everything baked in the framework, choose fastify or hapi and be happy about it. Now I like koa’s middleware framework and its flexibility.

To make things simpler lets first focus on sticking with the @koa/router, lets not rebuild yet another routers that does only ‘X’. This module does 3 things. This does not reimplement koa-router instead creates wrapper around koa-router (@koa/router). The lib might also support other routers out-of-box or with little tweak. (Feel free to fork :))

Now that we have got the router, we need to be able to support validation, and for lazy people like me I like to define a schema and get majority of the validation done through json schema. I have chosen ajv. Now that we have the schema, so the api doc can be derived from this schema. Before creating this lib, I started out writing jsdoc comments to generates swagger, not bad approach but it poses another burden of keeping jsdoc in sync to schema. Annotations aka decorators to create swagger doc has the same problem too, they are not directly linked to schema.

Thanks to fastify-swagger, fastify-oas, koa-mapper, koa-joi-swagger for inspiration.

  1. Define schema while defining router
  2. Schema will be used for validation of queryparams, headers, pathparams, request body.
  3. Schema will be used for generating openapi 3 json.

You can open this openapi3 json in http://editor.swagger.io to view the document. Optionally you can integrate swagger viewer in your application. Follow the index.js to see how it is done. Following snippet shows the main parts.

Quick start Example

npm install koa-router-ajv-swaggergen in your project. Additionally if want to setup swagger viewer npm install -D swagger-ui-dist

const Koa = require('koa')
const Router = require('@koa/router');
const Myrouter = require('koa-router-ajv-swaggergen');
const router = new Myrouter(new Router(), 'prefix');
const app = new Koa();// // requires `npm install -D swagger-ui-dist` to run swagger ui locally, otherwise comment this line
// Myrouter.setupSwaggerUI(router, 'prefix');
// // pass 'router' for json error response for this router, or pass 'app' for all errors to be converted to json
// Myrouter.setupJsonErrors(router);
router.get('/',
{
schema: {
summary: 'Minimal example',
description: 'This is a minimal setup',
querystring: {
type: 'object',
required: ['name'],
properties: {
name: { type: 'string' },
excitement: { type: 'integer' }
}
},
response: {
200: {
type: 'object',
properties: {
hello: { type: 'string' }
}
}
}
}
},
(ctx) => {
console.log('reqId', ctx.query.name);
ctx.body = { hello: ctx.query.name, excitement: ctx.query.excitement};
}
);
app.use(router.routes());app.listen(3000, () => {
console.log(`server listening on 3000`)
})

Open browser http://localhost:3000/prefix/openapi.json If you have setup swagger ui viewer open in browser http://localhost:3000/swagger

Some more details

const Router = require('@koa/router');
const Myrouter = require('../../routerwrapper');
const router = new Myrouter(new Router(), 'prefix'); // prefix is required to access different routes

Now you can use the router object like you would use koa-router i.e. even without a schema. But you can add schema which will start showing this route in swagger json and do schema validation.

router.get('/auth/get-user-profile', {
schema: {
querystring: {
username: { type: 'string' }
},
responses: {
'200': {
description: 'returns user details or redirect url /auth/login if not logged in'
},
}
}
}, async (ctx) => {
ctx.body = '';
})

Now the apidoc json is available at http://localhost:/prefix.openapi.json. You can have other router instances wrapped with a different prefix, and those will be available http://localhost:/anotherprefix.openapi.json.

Reusable schema

You can register schema globally in the router and reuse it in multiple routes

router.addSchema('commonSchema', {
type: 'object',
properties: {
hello: { type: 'string' }
}
})
router.post('/', {
schema: {
body: { $ref: '#/components/schemas/commonSchema' },
headers: { $ref: '#/components/schemas/commonSchema' }
}
}, (ctx) => { ctx.body = 'post /'; })

Validation

A more complete example, in fact this is directly taken from fastify-swagger documentation. Note the querystring and pathparams objects can be simplified as in this example. But if you need to control required then expanded format will need to be defined.

const bodyJsonSchema = {
type: 'object',
required: ['requiredKey'],
properties: {
someKey: { type: 'string' },
someOtherKey: { type: 'number' },
requiredKey: {
type: 'array',
maxItems: 3,
items: { type: 'integer' }
},
nullableKey: { type: ['number', 'null'] }, // or { type: 'number', nullable: true }
multipleTypesKey: { type: ['boolean', 'number'] },
multipleRestrictedTypesKey: {
oneOf: [
{ type: 'string', maxLength: 5 },
{ type: 'number', minimum: 10 }
]
},
enumKey: {
type: 'string',
enum: ['John', 'Foo']
},
notTypeKey: {
not: { type: 'array' }
}
}
}
// This is less verbose way of defining schema which works for querystring and params
// You can choose more verbose way of defining schema in the ajv way
const queryStringJsonSchema = {
name: { type: 'string' }, // add required: true if you want to make it required
excitement: { type: 'boolean' } // query strings are normally strings but it is coerced to boolean and then validated
}
const paramsJsonSchema = {
par1: { type: 'string' },
par2: { type: 'number' }
}
const headersJsonSchema = {
type: 'object',
properties: {
'x-foo': { type: 'string' }
},
required: ['x-foo']
}
const schema = {
body: bodyJsonSchema,
querystring: queryStringJsonSchema,
params: paramsJsonSchema,
headers: headersJsonSchema
}
router.post('/the/url/:par1/:par2', { schema }, (ctx) => {
ctx.body = 'post /the/url';
})

Integrating swagger viewer

Integrating swagger viewer is trivial. Get the static distribution instead of serving static directory from node_modules, serve it and rewrite content as required. I found this is the easiest way to mount into a different directory other than root. Swagger js and css’es do not understand relative paths

npm install -D swagger-ui-dist

Easy way is to use a provided helper method. Myrouter.setupSwaggerUI(router, 'prefix');

Here is what this helper function essentially does.

const swaggerUiAssetPath = require("swagger-ui-dist").getAbsoluteFSPath();
// Mount in your favourite path eg. '/swagger'
router.get('/swagger', ctx => {
ctx.type = 'html';
let fileAsString = fs.readFileSync(`${swaggerUiAssetPath}/index.html`);//
ctx.body = String(fileAsString).replace(/url\: \"https.*/m, `url: "prefix/openapi.json",`);
});
// Following static assets do not recognize relative paths.
router.get('/swagger-ui.css', ctx => {
ctx.type = "text/css"
ctx.body = fs.createReadStream(`${swaggerUiAssetPath}/swagger-ui.css`);
});
router.get('/swagger-ui-bundle.js', ctx => {
ctx.type = "application/javascript"
ctx.body = fs.createReadStream(`${swaggerUiAssetPath}/swagger-ui-bundle.js`);
});
router.get('/swagger-ui-standalone-preset.js', ctx => {
ctx.type = "application/javascript"
ctx.body = fs.createReadStream(`${swaggerUiAssetPath}/swagger-ui-standalone-preset.js`);
});

For developers of this library

Git clone the repository. Run npm install You run the node src\index-test.js or simply run npm run dev. It has a few samples to get started. Open browser at http://localhost:3000/swagger

Conclusion

This library is very new, it does have some shortcomings like schema may not be fully compliant with openapi3 specs. Local swagger viewer is more forgiving and does not complain. There are no tests yet. I just carved out a part of my other project into this library. So, I am going to fix any major defects. PRs are also welcome https://github.com/samarjit/koa-router-ajv-swaggergen.

Developer in UI, reactjs, angularjs, backend nodejs, java, springboot. Tinkerer of electronics. Loves to do everything hands on.