Distribute Spectral Style Guides with NPM

Distribute Spectral Style Guides with NPM

At some point in the growth of an organization there will come a time where
you've got too many different APIs with random different data formats, naming
conventions, security schemes, and generally it feels bizarre and frustrating
trying to call any API because you cannot share any code between API consumers.
If you're not there right now, good, you're reading this in time. If you're
already there, yeah it sucks doesn't it, and this article will help you work
your way out of that pit over time.

API Style Guides have always been a crucial part of API Governance, but they
usually come in the form of a giant manifesto, written by the most
opinionated experienced person at your organization. Often they're ignored, and when they are read it's impossible to remember all the decisions, especially as some may change over time. Now API style guides can be automated, and if you're describing APIs with
OpenAPI you can use excellent tools like Stoplight Spectral, which is the natural successor to the previously popular Speccy (a tool I built at WeWork but was promptly abandoned when I left).

Both tools let you define all the rules you need for your API Style Guide in
YAML or JSON, and distribute them via publicly via URLs, or smush them into the filesystem somehow, but Spectral offers a fantastically powerful new way of building and distributing your style guides: you can build them in JavaScript, write tests for the rules, use whatever libraries you feel like, and distribute via NPM so that you can version your API Style Guides.

To demonstrate I've created the APIs You Won't Hate: Style Guide, which will help everyone
make their APIs better using tips we've been sharing on this blog and in the
books for the last decade.

Create an API Style Guide in JavaScript

Let's use the JavaScript format for the ruleset instead of the JSON or YAML
formats that people have traditionally used when working with Spectral.

mkdir style-guide
cd style-guide

Then we will need to create some dependencies.

npm install --save @stoplight/spectral-functions
npm install --save @stoplight/spectral-formats

If you use Spectral already, you may be familiar with the core functions and
core formats it bundles with. Don't worry if not, we'll get into that in a moment.

Create a JS file that will hold your ruleset, possibly called ruleset.js or
spectral.js, whatever you feel like calling it.

touch ruleset.js

In that file we're going to start building the structure for the ruleset.

import { enumeration, truthy, falsy, undefined as undefinedFunc, pattern, schema } from "@stoplight/spectral-functions";
import { oas2, oas3 } from "@stoplight/spectral-formats";

export default {
  rules: {
    // ... your rules go here
  }
}

This is a bit more bootstrapping than if you just start writing random YAML into
a file, but seeing as it's just plain old JavaScript you get all the benefits of
working in your favourite IDE, which means type hinting, auto-complete, it'll
let you know if your function is invalid, and you can import your own functions
to power whatever rules you can image.

Let's make some rules.

import { enumeration, truthy, falsy, undefined as undefinedFunc, pattern, schema } from "@stoplight/spectral-functions";
import { oas2, oas3 } from "@stoplight/spectral-formats";

export default {
  rules: {

    'api-home': {
      description: 'APIs MUST have a root path (`/`) defined.',
      message: 'Stop forcing all API consumers to visit documentation for basic interactions when the API could do that itself.',
      given: "$.paths",
      then: {
        field: "/",
        function: truthy,
      },
      severity: 'warn',
    },

    'api-home-get': {
      description: 'APIs root path (`/`) MUST have a GET operation.',
      message: "Otherwise people won't know how to get it.",
      given: "$.paths[/]",
      then: {
        field: "get",
        function: truthy,
      },
      severity: 'warn',
    },

    // Author: Phil Sturgeon (https://github.com/philsturgeon)
    'no-numeric-ids': {
      description: 'Please avoid exposing IDs as an integer, UUIDs are preferred.',
      given: '$.paths..parameters[*].[?(@property === "name" && (@ === "id" || @.match(/(_id|Id)$/)))]^.schema',
      then: {
        function: schema,
        functionOptions: {
          schema: {
            type: "object",
            not: {
              properties: {
                type: {
                  const: "integer"
                }
              }
            },
            properties: {
              format: {
                const: 'uuid'
              }
            }
          }
        }
      },
      severity: 'error',
    },
    
    // Author: Nauman Ali (https://github.com/naumanali-stoplight)
    'no-global-versioning': {
      description: 'Using global versions just forces all your clients to do a lot more work for each upgrade. Please consider using API Evolution instead.',
      message: 'Server URL should not contain global versions',
      given: "$.servers[*].url",
      then: {
        function: pattern,
        functionOptions: {
          notMatch: '/v[1-9]'
        }
      },
      formats: [oas3],
      severity: 'warn',
    }
  }
};

The format of these rules might look familiar to anyone used to creating
rulesets
in JSON/YAML, it's just the JavaScript version. Functions are actually referenced as functions instead of string names for functions, which removes a lot of the confusion around where custom functions live and how they're used.

Shove some callbacks in. It's JavaScript afterall!

These rules are making sure you have something showing on your API "root route" https://example.com/api/, making sure it's got a GET defined (consider using IETF Draft DRFC: Home Documents for HTTP APIs), and you're not using global versioning which is a giant pain in the ass. Your milage may vary, but that's the point of writing your own style guide. This is mine. There's also a rule in there pointing out that using auto incrementing IDs in an API are generally a pretty bad idea.

There's loads of other rules in there which you can feel free to copy and paste into a ruleset for your API Style Guide. Head over to ruleset.ts and have a root around to see if you want any, just make sure you are attributing the content (I'd have used the DBAD license if I didn't think I was gonna get moaned at about it).

Publish Your Spectral Ruleset to NPM

Before I can publish to the NPM public repository I'll need to create a
package.json that contains a few specific options.

npm init

This will ask a whole bunch of questions via CLI prompts, and you can answer
them a little something like this:

package name: (style-guide) @apisyouwonthate/style-guide
version: (1.0.0)
description: Make your HTTP APIs better, faster, stronger, whether they are still being designed (API Design-First) or your organization has flopped various mismatched APIs into production and now you're thinking some consistency would be nice. Using Spectral and OpenAPI.
entry point: (index.js) ruleset.js
test command:
git repository: https://github.com/apisyouwonthate/style-guide
keywords: openapi, openapi3, openapi31, api-design
author: Phil Sturgeon <phil@apisyouwonthate.com>
license: (ISC) MIT

With that package.json created, the next step is to grab the dependencies we
know it'll need.

npm install --save @stoplight/spectral-functions
npm install --save @stoplight/spectral-formats

Time to yeet this up to the public NPM repository so everyone can use it in all their APIs, and as a basis for their own organization-wide API Style Guides! Read the full documentation on how to publish modules to
NPM
(and you'll need an account, etc)

npm login
npm publish --access public

If that's worked, you'll be able to see it up on NPM. My package is up on NPM over here.

Using an NPM published package

Using a package that's been published to NPM works the same as any using other Spectral ruleset. IIf you're working with Spectral somewhere you can install an NPM module (e.g.: Spectral CLI or Spectral JS) you can install the package with NPM in the CLI and point Spectral to the rulesets you want to use.

cd ~/src/<your-api>

npm install --save -D @stoplight/spectral-cli

npm install --save -D @apisyouwonthate/style-guide

echo 'extends: ["@apisyouwonthate/style-guide"]' > .spectral.yaml

Now when you run spectral lint openapi.yaml you'll get all my opinions being shouted at your API, and if you're using API Design First maybe it will help you avoid wasting time writing a bunch of code that you'll just have to change later.

/Users/phil/src/protect-earth-api/api/openapi.yaml
  18:7   warning  api-health               Creating a `/health` endpoint is a simple solution for pull-based monitoring and manually checking the status of an API.  paths
  18:7   warning  api-home                 Stop forcing all API consumers to visit documentation for basic interactions when the API could do that itself.           paths
  36:30  warning  no-unknown-error-format  Every error response SHOULD support either RFC 7807 (https://tools.ietf.org/html/rfc6648) or the JSON:API Error format.   paths./v1/orders.post.responses[401].content.application/json
  96:30  warning  no-unknown-error-format  Every error response SHOULD support either RFC 7807 (https://tools.ietf.org/html/rfc6648) or the JSON:API Error format.   paths./v1/orders/{order}.get.responses[401].content.application/json
 112:30  warning  no-unknown-error-format  Every error response SHOULD support either RFC 7807 (https://tools.ietf.org/html/rfc6648) or the JSON:API Error format.   paths./v1/orders/{order}.get.responses[404].content.application/json

Thanks for making Phil's API better, Phil!

Using NPM Rulesets in Stoplight Studio & Platform

If you're using Stoplight Platform, Stoplight Studio Desktop, VSCode Spectral, etc. then you can still use the NPM-based API Style Guides, but as there's no way to get NPM involved you can load it with this One Amazing Trick Doctors Don't Want You To Know About.

extends:
  - https://unpkg.com/@apisyouwonthate/style-guide@1.1/ruleset.js

Using Unpkg or similar means you can load the ruleset with whatever version
specificity you want (@1, @1.1, @1.1.2) and it'll work anywhere that
Spectral does.

Looking at this, you might be wondering about versioning, and that naturally
brings up the question of testing. Both possible, and both better off as their own blog post.

Other API Style Guides & Rulesets

More and more companies and organizations are publishing their API Style Guides as Spectral rulesets, so I've been putting them together in a simple little repo which will one day become a "marketplace".

Here are some of the best:

Use these rules as a basis for your style guides, and make sure your APIs are
useful before you waste time building something rubbish.

Host API Style Guides on Stoplight

If mucking around with NPM isn't up your alley, as always Stoplight have a
convenient hosted solution you can use that requires a bit less duct-tape and string.

Create a Style Guide Project on Platform, then you can build and share a ruleset in a GUI experience instead of writing JavaScript. You can even import your existing Spectral
rulesets
to be a Style Guide Project, and leave your awkward JSON/YAML rulesets behind for good.

Either way, all the APIs at your organization can use these style guides to get feedback as people are designing their APIs, improving API Governance, and wasting less time worrying about small fry so you can talk about far bigger and more important things in your API Design Review sessions.