← All news
configuration4 min read

Your API documentation is a map of your API, and it's sitting at a public URL

By Seaworthy · 27 August 2026

A wide open metal gate

Photo by Jan Tinneberg on Unsplash

Every framework that generates OpenAPI docs makes wiring them up almost suspiciously easy:

const swaggerUi = require('swagger-ui-express')
const swaggerDocument = require('./openapi.json')

app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument))

Three lines, and now anyone who's ever built against your API has a browsable, interactive reference. So does anyone who hasn't, and never should have.

Documentation and access control are two different decisions

Generating API documentation is unambiguously good practice, it's how a frontend team, a mobile team, or a third-party integration partner knows what your API actually does without reading the route handlers directly. The mistake isn't generating it, it's mounting it at a public path with no authentication, which is what /api-docs on a production domain with no middleware in front of it amounts to: a complete, interactive map of your API surface, available to anyone who finds the URL, and URLs like /api-docs, /swagger, and /api/docs are exactly the kind of thing automated scanners check for on every domain they touch.

What that map actually contains is more than an index of endpoint names. A reasonably complete OpenAPI spec documents every parameter each endpoint accepts, including internal ones that were never meant to be called from outside, request and response schemas that describe your internal data model field by field, and often authentication scheme details, header names, token formats, that are useful context for someone trying to forge a valid-looking request. None of this is a vulnerability by itself. It's reconnaissance, and reconnaissance is the first step of an attack, not a coincidence, it's the part that makes every subsequent step faster and more targeted.

An endpoint that would otherwise require guessing a parameter name or an internal field, isAdmin, internalNotes, _debugOverride, stops requiring any guessing at all once its full schema is sitting in a public Swagger UI.

Put it behind the same door as everything else internal

function requireInternalAccess(req, res, next) {
  if (req.headers['x-internal-token'] !== process.env.INTERNAL_DOCS_TOKEN) {
    return res.status(404).send('Not found')
  }
  next()
}

app.use('/api-docs', requireInternalAccess, swaggerUi.serve, swaggerUi.setup(swaggerDocument))

Returning 404 rather than 401 for the unauthenticated case means the endpoint doesn't even confirm that documentation exists at that path to someone probing without credentials, which is a small thing but worth doing consistently across anything internal-only.

If the API docs genuinely need to be public, because you're a platform product and third parties integrate against a stable public contract, that's a legitimate case, and the fix there isn't access control, it's making sure the spec you're serving publicly only documents the public surface. That usually means maintaining the public spec as a deliberately separate, smaller file from whatever your framework auto-generates from every route in the codebase, rather than serving the auto-generated one directly and hoping nothing internal made it in.

# check whether your production docs endpoint is reachable without credentials
curl -s -o /dev/null -w "%{http_code}\n" https://your-app.example.com/api-docs

A 200 means it's open. If that's not intentional, it's worth checking what's actually in the spec next, not just gating the route.

Why "make the docs work" and "make the docs public" get conflated

Getting Swagger UI running is a genuine milestone when you're building an API, it's the point where the API stops being something only you can reason about and becomes something a frontend or a partner team can self-serve against. The three-line setup snippet that gets you there, straight from the library's own README, mounts the docs at a plain route with no auth, because the README's job is showing you the shortest path to a working demo, not modeling your production access requirements. Whether that route belongs behind auth in your specific deployment is a decision the snippet has no way to make for you.

Seaworthy flags API documentation routes (Swagger, OpenAPI, GraphQL introspection UIs) mounted without authentication middleware, particularly when the underlying spec includes internal-only endpoints or fields.

This article was generated by AI and summarises publicly available sources.

From the makers of Seaworthy

Seaworthy scans your repo for the issues covered in articles like this one.

Security gaps, exposed secrets, and misconfigurations — caught before you deploy. Free to run, no account needed.

$ npx seaworthycode