Wiring up an API server with Express and Swagger
ExpressJS
is the go-to framework for writing API servers with NodeJS
, and Swagger
is a brilliant way to specify the details of your API in a way that allows you to ensure the API is consistent, documented, and testable, using a range of handy tools.
To avoid repetition it’s desirable to also use the Swagger definition of your API to tell Express how to map incoming paths to route controller functions.
To make this easy I have written a small package called swagger-routes-express
(updated recently to support OpenAPI 3
in addition to Swagger 2
).
Example
The following is a simple API defined with Swagger in a file my-api.yml
, placed at the root of the project.
swagger: "2.0"
info:
description: Something about the API
version: "1.0.0"
title: My Cool API
basePath: "/api/v1"
schemes:
- "https"
paths:
/ping:
get:
tags:
- "root"
summary: "Get Server Information"
operationId: "ping"
produces:
- "application/json"
responses:
200:
description: "success"
schema:
$ref: "#/definitions/ServerInfo"
definitions:
ServerInfo:
type: "object"
properties:
name:
type: "string"
description:
type: "string"
version:
type: "string"
uptime:
type: "number"
There’s one path, GET /ping
, that returns a JSON
response:
{
"name": "My Cool API",
"description": "Something about the API",
"version": "1.0.0",
"uptime": 101
}
The Swagger doc defines an operationId
for the GET /ping
route called ‘ping’
.
In our server’s src
folder, we’ll define the ping
controller as follows
src/api/ping.js
const {
name,
version,
description
} = require('../../package.json')const ping = (req, res) => {
res.json({
name,
description,
version,
uptime: process.uptime()
})
}module.exports = ping
and in src/api/index.js
we'll just ensure it’s exported:
const ping = require('./ping')
module.exports = { ping }
Define an async
function to create and configure the Express app itself.
This will use the swagger-parser
library to parse our swagger api YAML
file (hence the need to be async
) and pass the parsed info to swaggerRouter
to configure it.
src/makeApp.js
const express = require('express')
const SwaggerParser = require('swagger-parser')
const swaggerRoutes = require('swagger-routes-express')
const api = require('./api')const makeApp = async () => {
const parser = new SwaggerParser()
const apiDescription = await parser.validate('my-api.yml')
const connect = swaggerRoutes(api, apiDescription) const app = express()
// do any other app stuff,
// such as wire in passport, use cors etc. // then connect the routes
connect(app) // add any error handlers last
return app
}module.exports = makeApp
In src/index.js
we start the server:
const makeApp = require('./makeApp')makeApp()
.then(app => app.listen(3000))
.then(() => {
console.log('Server started')
})
.catch(err => {
console.error('caught error', err)
})
Voila — Run that and calls to GET /ping
will invoke the ping
controller.
Adding route-specific security middleware
Most APIs need to add some level of authentication and access control to specific routes. In the Swagger document you can add security information on a path-by-path basis.
Add a GET /api/v1/things
path to the Swagger document:
/things:
get:
summary: "Find things"
description: "Returns a list of things"
operationId: "v1/things/listThings"
produces:
- "application/json"
responses:
200:
description: "success"
schema:
$ref: "#/definitions/ArrayOfThings"
security:
- slack:
- "identity.basic"
- "identity.email"
In a real application you’d need to add the definition of ArrayOfThings
, and the security definitions but, for brevity, ignore them.
Add a listThings
controller to src/api/index.js
, mapping v1/things/listThings
to v1_things_listThings
(you can change the pathSeparator
via an option
, see below).
const ping = require('./ping')
const v1_things_listThings = require('./v1/things/listThings')module.exports = { ping, v1_things_listThings }
You need to tell the swaggerRouter
how to associate the security scopes defined in the document with actual authentication middleware. Normally you’d use passport to set up standards based authentication middleware, but ultimately the middleware is just a function with a req, res, next
signature. Let’s define one in src/auth/isAllowed.js
that simply looks for the text 'LET THE RIGHT ONE IN'
in the standard Authorization
request header.
module.exports = (req, res, next) => {
if (req.get('authorization') === 'LET THE RIGHT ONE IN') {
return next()
}
return res.status(401).end()
}
Now in our makeApp function we need to pass in some options to swaggerRouter
.
const connect = swaggerRoutes(api, apiDescription, {
scopes: {
'identity.basic,identity.email': isAllowed
}
})
Now the connect
function will know to associate paths with the scopes identity.basic
and identity.email
with the isAllowed
middleware.
What about the base path?
Unlike the ping
route defined above, the /things
is being concatenated with the API’s basePath
, 'api/v1'
. The swaggerRouter
understands that the ping
route is not to go under the basePath because of the root
tag. You can change this in the options outlined below.
What about missing controllers?
If the Swagger doc defines an operationId
that cannot be mapped to an existing route controller function then the connector
will insert a default notImplemented
function in its place that returns res.status(501).end()
.
If no operationId
is associated with a path then the connector
will insert a default notFound
function that returns res.status(404).end()
.
You can override the default functions via options passed to swaggerRouter
as outlined below.
Options
The full set of allowed options for the swaggerRouter
function is:
{
notImplemented, // a default function is supplied,
notFound, // a default function is supplied
scopes: {},
apiSeparator: '_',
rootTag: 'root'
}
Other tools
The swagger-express-validator
validates the inputs and outputs of your API route controllers against their Swagger definitions. When combined with comprehensive integration tests you assert the correctness of your server.
The swagger-ui-express
utility serves up your Swagger definition doc as an interactive web user-interface, ensuring that 3rd parties accessing your API have access to documentation that’s consistent with its code.
Links
- Express
- Swagger
swagger-routes-express
in NPMswagger-routes-express
source code in GitHubswagger-express-validator
swagger-ui-express
—
Like this but not a subscriber? You can support the author by joining via davesag.medium.com.