A tool to generate and register Restify or Express route handlers from a Swagger 2.0 (OpenAPI) specification.
Requires Node v4.0+
const swaggerRoutes = require('swagger-routes')
const express = require('express')
const app = express()
swaggerRoutes(app, {
api: './api.yml',
handlers: './src/handlers',
authorizers: './src/handlers/security'
})
app.listen(8080)
const swaggerRoutes = require('swagger-routes')
const restify = require('restify')
const server = restify.createServer()
swaggerRoutes(server, {
api: './api.yml',
handlers: './src/handlers',
authorizers: './src/handlers/security'
})
server.listen(8080)
api
: path to your Swagger spec, or the loaded spec reference.docsPath
: url path to serve your swagger api json. Defaults to/api-docs
.handlers
: directory where your handler files reside. Defaults to./handlers
. Can alternatively be a function to return a handler function given an operation.authorizers
: directory where your authorizer files reside. Defaults to./security
. Can alternatively be a function to return an authorizer middleware given a swagger security scheme.maintainHeaders
: Keeps your generated handler doc headers in sync with your Swagger api. Default is false.
You have the option to define and maintain a handler file for each Swagger operation, or alternatively provide a factory function which creates a handler function given an operation.
Using individual handler files is a good choice if each handler needs unique logic to deal with an operation request.
A handler file must be named after the Swagger operation it handles e.g. listPets.js
.
All handler files must reside in the same directory, unless the group
option is enabled,
in which case the handler file should sit under a folder of its primary tag name (see Generating Handler Files below).
A function called handler
should be exported to deal with an incoming operation request.
exports.handler = function listPets(req, res, next) {
}
You also have the option to export a middleware
function to be executed before the handler.
exports.middleware = preprocess
function preprocess(req, res, next) {
next()
}
Middleware can be an ordered list.
exports.middleware = [
function preprocess1(req, res, next) { next() },
function preprocess2(req, res, next) { next() }
]
To save you some time there's a bundled tool to generate handler files based on operations in your Swagger spec, together with a Mustache template.
This tool is on by default so check your handlers folder the first time you run swaggerRoutes
and
it should be poulated with handler stubs for each operation defined in your Swagger document.
Each time you start your app swaggerRoutes
will see if you have any missing operation handlers and generate
stub handler for any which are. If a handler file exists it won't be touched, i.e. this is non-destructive so you are free
to edit them.
Note that if you turn on the syncHeaders
option then the header of your handler files will be
updated each run based on your Swagger api. This keeps your handler documentation up to date so
you can easily see what parameters accompany a request for a given operation. It will overwrite
any edits you make to the header so only turn on if you don't plan on manually editing them.
When a re-run finds handlers no longer in use they will be renamed with an _
prefix, so
listPets.js
would become _listPets.js
. This allows you to identify handlers no longer in use
and remove / rename them if you wish.
If you later enable a handler again in your spec and re-run, then the underscore will be removed.
Note that this feature of prefixing removed handlers is only currently supported when the group
options is not enabled.
The default template is defined here but you can supply your own by expanding the handlers
option e.g.
{
...
handlers: {
path: './src/handlers',
template: './template/handler.mustache', // can also be set with a loaded template
getTemplateView: operation => operation, // define the object to be rendered by your template
create: operation => (req, res) => {}, // see Handler Factory section for details
generate: true, // hander file generation on by default
group: false // when true each handler file will be placed under a directory named after its primary tag
}
}
The factory function is a better option to a file if handlers are quite similar e.g. delegate their request processing onto service classes.
You can define handlers
as a function when registering your routes. It receives a Swagger operation and returns the request handler responsible for dealing with it.
const swaggerRoutes = require('swagger-routes')
swaggerRoutes(app, {
api: './api.yml',
handlers: createHandler
})
function createHandler(operation) {
return function handler(req, res, next) {
res.send(operation.id)
}
}
If a handler function is returned then it will take precedence over a handler file for the same operation.
Just as a file handler can define route middleware, so can createHandler
.
function createHandler(operation) {
return {
middleware: function preprocess(req, res, next) { next() },
handler: function handler(req, res, next) { res.send(operation.id) }
}
}
As before, route middleware can be an ordered list.
function createHandler(operation) {
return {
middleware: [
function preprocess1(req, res, next) { next() },
function preprocess2(req, res, next) { next() }
],
handler: function handler(req, res, next) { res.send(operation.id) }
}
}
When your Swagger api specifies one or more security schemes then routes which opt into one or more of these schemes can be protected by authorizer middleware.
Just like handlers, you can define an authorizer in a file or via a factory.
The file should be named after the security scheme it protects e.g. petstore_auth.js
, and reside in the directory path defined by the authorizers
option. It should export a single middleware function to authorize a request.
module.exports = function petstore_auth(req, res, next) {
const token = decodeToken(req.headers.authorization)
if (token) {
const scopes = getTokenScopes(token)
next(req.verifyScopes(scopes))
} else {
const error = new Error('Unauthorized')
error.status = error.statusCode = 401
next(error)
}
}
The above is one example of how this can work.
As you can see a verifyScopes
function is supplied to the req if the security scheme is OAuth2.
It takes an array of scopes you decode from the authenticated request and verifies that the
required scope(s) defined be the scheme are present. If they're not a 403 Forbidden
error is returned.
Remember if no credentials are supplied a 401 Unauthorized
should be returned.
Much like handler files, authorizer file stubs will be generated and managed for you too.
The default template is defined here
but you can supply your own by expanding the authorizers
option e.g.
{
...
authorizers: {
path: './src/handlers/security',
template: './template/authorizer.mustache', // can also be set with a loaded template
getTemplateView: operation => operation, // define the object to be rendered by your template
create: operation => (req, res) => {}, // see Authorizer Factory section for details
generate: true // authorizer file generation on by default
}
}
const swaggerRoutes = require('swagger-routes')
swaggerRoutes(app, {
api: './api.yml',
authorizers: createAuthorizer
})
function createAuthorizer(schemeId, securityScheme) {
return function authorizer(req, res, next) {
const token = decodeToken(req.headers.authorization)
if (token) {
const scopes = getTokenScopes(token)
next(req.verifyScopes(scopes))
} else {
const error = new Error('Invalid access token')
error.status = error.statusCode = 401
next(error)
}
}
}
Each incoming request which makes it to a handler will be run through request validation middleware. This executes JSON Schema validation on the request to ensure it meets the Swagger specification you've defined. A failure to meet this requirement will cause the request to fail and the handler not to be executed.
Statically setting the host
property of your Swagger api can be error prone if you run the api
in different environments (QA, Staging, Production), that's why I'd recommended removing its
definition from your specification. This will by default then
resolve to the host, including port, the spec
is served from.
If this still isn't sufficient you have a couple of other options.
- Set
API_HOST
environment variable for your node instance. SwaggerRoutes will pick this up and use it. - Set the
app.swagger.host
manually from within your app after you've calledswaggerRoutes
.
const server = app.listen(3000, '0.0.0.0', () => {
app.swagger.host = `${server.address().address}:${server.address().port}`
})
authorizer middleware
If there are security restrictions on a route then an authorizer for each will need to verify the rights attached to the request.custom middleware
If the route defines one or more middleware these will be executed in order.validation middleware
The incoming request will now be validated against the Swagger spec for the given operation.handler
Assuming all previous steps pass, the handler is now executed.
You may be in the situation where you have a Swagger definition for each major version of your api. If this is the case, and you want to handle each on the same server, then you are free to register more than one spec.
swaggerRoutes(server, {
api: './api-v1.yml',
handlers: './src/handlers/v1',
authorizers: './src/handlers/v1/security'
})
swaggerRoutes(server, {
api: './api-v2.yml',
handlers: './src/handlers/v2',
authorizers: './src/handlers/v2/security'
})
You'll need to ensure that there's no conflict in route paths between each. The best
way to do that would be to add a unique basePath
to each spec, say /v1
, /v2
etc.
An operation object inherits its properties from those defined in the Swagger spec.
There are only a few differences / additions.
id
: ReplacesoperationId
.path
: The route path of this operation.method
: The http methodconsumes
: Populated with the top levelconsumes
unless the operation defines its own.produces
: Populated with the top levelproduces
unless the operation defines its own.paramGroupSchemas
: JSON Schema for each param group ('header', 'path', 'query', 'body', 'formData') relevant to the operation.
Inspiration for this library came primarily from time spent using swaggerize-express. It's a great library which you should check out.
My reasoning behind writing a new, alternate implementation was the wish to base all routing off operation ids and not paths. This aligns with how Swagger client code gen works, making it easier to see your client SDKs and server code base as a whole.
I also wanted to automate away much of the boilerplate code being written and support Restify and Express in a single library, given their similarities.