I program my home computer. Beam myself into the future. (Photo by the author.)

Wiring up an API server with Express and Swagger

Dave Sag
Published in
4 min readAug 9, 2018

--

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

Like this but not a subscriber? You can support the author by joining via davesag.medium.com.

--

--