# next-swagger-doc: generating an OpenAPI spec from your Next.js API routes

> next-swagger-doc reads the JSDoc annotations you already wrote on your Next.js API routes and turns them into an OpenAPI 3 specification, with an auto mode that derives operations from App Router route files. The interesting parts are the deployment caveats, where the tool deliberately refuses to scan the build output.

**jellydn/next-swagger-doc** — This package reads your JSDoc-annotated source code on NextJS API route and generates an OpenAPI (Swagger) specification.

- Repository: https://github.com/jellydn/next-swagger-doc
- Website: https://next-swagger-doc-demo.productsway.com/api-doc
- Stars: 520 · Forks: 37
- Language: TypeScript
- License: MIT
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/jellydn-next-swagger-doc

## The premise: the annotations are already in your code

This package reads your JSDoc-annotated source code on NextJS API route and generates an OpenAPI (Swagger) specification. The stated prerequisites are Nextjs >= 9 and Node >= 18, and the project is MIT licensed with a demo at next-swagger-doc-demo.productsway.com/api-doc and documentation at next-swagger-doc.productsway.com.

The idea is a small equation, written out in the README as nextjs plus swagger-jsdoc equals next-swagger-doc. The JSDoc comment block above a route handler is already a place where people describe what an endpoint does, and swagger-jsdoc is a library that turns exactly that kind of comment into an OpenAPI fragment. The package is the glue that points swagger-jsdoc at your route folder and merges the result into one document.

The payoff is that the documentation cannot drift from the code, because it is derived from the code. A route you delete disappears from the spec. A route you rename changes the spec. What it cannot capture is anything you did not write down.

The project is by Huynh Duc Dung under the jellydn organisation, the primary language is TypeScript, the default branch is main, and the last push was on 2026-10-20. The recent releases are v0.5.0 on 2026-08-26, v0.4.2-beta.0 on the same day and v0.4.2-0 from 2025-02-17, so the 0.x line is still moving, and the presence of a beta in the same release window as the stable suggests the 0.4 to 0.5 transition was substantial.

The repository has a good structure for a package like this. There is a src/ directory, a test/ directory, an examples/ directory with four separate example projects named next13-simple, next14-app, next15-app and next16-app, a vitest.config.ts, a biome.json for linting and formatting, a cspell.json and cspell-tool.txt for spell checking, a pnpm-lock.yaml, a renovate.json for dependency updates, a .changie.yaml and a .changes/ directory for changelog entries, and a .pre-commit-config.yaml.

The four example projects are the detail that tells you the support matrix. A separate example for each of four successive Next.js generations is what a package that has to keep working across framework upgrades actually looks like.

## Building the spec, and the definition object that describes your API

The first step of the documented setup is to create a file lib/swagger.ts that calls createSwaggerSpec with the API folder and a definition:

```javascript
import { createSwaggerSpec } from "next-swagger-doc";

export const getApiDocs = async () => {
  const spec = createSwaggerSpec({
    apiFolder: "app/api", // define api folder under app folder
    definition: {
      openapi: "3.0.0",
      info: {
        title: "Next Swagger API Example",
        version: "1.0",
      },
      components: {
        securitySchemes: {
          BearerAuth: {
            type: "http",
            scheme: "bearer",
            bearerFormat: "JWT",
          },
        },
      },
      security: [],
    },
  });
  return spec;
};
```

Two things in that object are worth understanding. The definition is the static skeleton of the document, and it is where you describe the API as a whole rather than endpoint by endpoint: the OpenAPI version, the title and version of your API, and the components section where security schemes are declared. The BearerAuth block with a bearer scheme and a JWT bearer format is a standard OpenAPI 3 security scheme, and declaring it here is what lets individual endpoints reference it.

The security array is set to empty in the example, which is a deliberate choice for a document that is not describing a secured API. Setting it non-empty at the document level is how you apply a scheme to every operation by default, with per-operation overrides for the exceptions.

The apiFolder option is the pointer to where your routes live, and the comment in the example notes that this is the api folder under the app folder, which is the App Router layout rather than the older pages/api directory. The function is async and returns the spec, so it is designed to be called from a server component that then hands the result to a client component.

After creating the spec, the documented flow is to create a Swagger UI component, then a page that imports the spec and renders the UI, and finally to add a Swagger comment to a route.

## The annotation itself, and what autoDoc does without it

The manual annotation is a JSDoc block with a @swagger tag above the route handler:

```javascript
/**
 * @swagger
 * /api/hello:
 *   get:
 *     description: Returns the hello world
 *     responses:
 *       200:
 *         description: Hello World!
 */
export async function GET(_request: Request) {
  // Do whatever you want
  return new Response('Hello World!', {
    status: 200,
  });
}
```

The path is written inside the annotation rather than derived from the file location, which is what makes the format flexible enough to describe routes that do not map one to one onto files. The method is a key under the path, and responses carry their own descriptions.

The auto mode removes the need for that block. Set autoDoc: true to generate basic operations for App Router route.ts files. The path is derived from the route directory, dynamic segments such as [id] become {id}, and exported HTTP handlers such as GET and POST become operations.

The generated operations include a default successful response, and the README is clear about when that is not enough: use a manual @swagger block when an endpoint needs request, response or other operation metadata. Manual operations take precedence over generated ones.

That precedence rule is the design centre of the auto mode. The generated operations are a floor, not a ceiling: every endpoint appears in the spec with a sensible path and method, and you override only the ones that need more. For an application with forty routes where three need detailed schemas, that is the difference between annotating three endpoints and annotating forty.

The auto mode configuration itself is small:

```javascript
const spec = createSwaggerSpec({
  apiFolder: 'app/api',
  autoDoc: true,
  definition: {
    openapi: '3.0.0',
    info: { title: 'My API', version: '1.0.0' },
  },
});
```

The one thing worth noting is the shape of the dynamic segment conversion. A route file at app/api/users/[id]/route.ts becomes the path /api/users/{id}, following the OpenAPI convention of naming a path parameter in braces rather than in square brackets, so the generated spec is directly usable rather than needing a translation step.

## The rendering step, and the Strict Mode problem behind the client-only import

The second step creates a component that renders the spec, and the README spends a surprising amount of text on one line of it. Load swagger-ui-react on the client only.

```javascript
'use client';

import dynamic from 'next/dynamic';
import 'swagger-ui-react/swagger-ui.css';

const SwaggerUI = dynamic(() => import('swagger-ui-react'), { ssr: false });

type Props = {
  spec: Record<string, unknown>,
};

function ReactSwagger({ spec }: Props) {
  return <SwaggerUI spec={spec} />;
}

export default ReactSwagger;
```

The reason is given explicitly: Next.js enables React Strict Mode by default, and swagger-ui-react still uses UNSAFE_componentWillReceiveProps in components such as ExamplesSelect and ParameterRow. The README notes that the warning comes from Swagger UI, not from this library, which is the kind of attribution that saves you an afternoon of looking for a bug in the wrong place.

The pattern itself is the standard Next.js answer to a browser-only dependency: the use client directive marks the module as a client component, and dynamic with ssr: false stops the library from being imported on the server at all. The type of the spec is Record<string, unknown>, which is loose on purpose because the spec is generated at runtime and has no compile-time shape.

The README also says the swagger-ui-react library is not mandatory: feel free to employ any alternative swagger UI library, such as stoplightio/elements or Scalar, and there is an example in the example folder using Stoplight Elements. Since the generated artefact is an OpenAPI document rather than a rendered page, the UI layer is genuinely interchangeable.

The third step is then trivial: the page imports the spec and the component and renders it, in a file app/api-doc/page.tsx. The navigation instruction is to go to localhost:3000/api-doc, or wherever the application is hosted, and the swagger UI appears.

## The deployment caveats, and the CLI for standalone output

Two deployment notes in the README are the most valuable part of it, because both describe failures that look like the tool is broken rather than like a configuration mismatch.

The first is about Vercel builds. createSwaggerSpec does not glob .next during next build. Walking that folder while Next.js is compiling it can fail Vercel with ENOENT: .next/export-detail.json. Source API files and public OpenAPI files are scanned instead, and scanBuildOutput: true should be set only when you intentionally want compiled .next/server files included.

So the tool deliberately refuses to look in the build output directory by default, and the reason is a race condition rather than a preference. The build writes that folder while the spec generation walks it, and on a local machine the timing usually works out, while on Vercel it does not. An ENOENT on a file inside .next is a confusing error to receive from a documentation tool, and the fix is not to enable the risky option but to leave it off.

The second is about Next.js standalone output. output: 'standalone' copies a minimal server, and source app/api files are not included, so scanning routes at runtime yields an empty spec.

That is the more fundamental problem, and it has a different answer. If your server does not contain your route files, there is nothing for the tool to read at runtime, so the document has to be produced earlier, at build time, and shipped as a file. The CLI does that:

```sh
npx next-swagger-doc-cli next-swagger-doc.json --output public/swagger.json
```

And the spec is then loaded from that file with a specFile option, alongside the usual apiFolder and autoDoc settings:

```javascript
const spec = createSwaggerSpec({
  specFile: 'public/swagger.json',
  apiFolder: 'app/api',
  autoDoc: true,
  definition: {
    openapi: '3.0.0',
```

The public/ directory is where it goes because that is the directory copied into the standalone output, which is what makes the generated file actually reachable by the running server.

The package ships that CLI as a binary, next-swagger-doc-cli, pointing at dist/cli.js, so the command is available from an npx invocation without a global install. The package is also dual published, with require pointing at dist/index.cjs and import at dist/index.js, and a types entry of dist/index.d.ts, so it works in both module systems.

Together, these two notes cover the two deployment shapes that break at different points, and both are handled by the same principle: generate from source when the source is present, and ship a pre-generated file when it is not.

## Conclusion

Adopt next-swagger-doc if your Next.js project already annotates its API routes with JSDoc and you want a generated OpenAPI document plus a browsable UI without maintaining a spec by hand. Do not adopt it as a runtime documentation server for a standalone build, because the tool explicitly does not glob the .next directory and a standalone output does not include your source app/api files, so the spec comes back empty. Before you deploy, check whether your build is standalone, and if it is, generate the document at build time with the CLI and load it from public.

## FAQ

### What does next-swagger-doc do?

It reads your JSDoc-annotated source code on NextJS API routes and generates an OpenAPI (Swagger) specification, and the project describes itself as nextjs plus swagger-jsdoc. The specification is produced by the createSwaggerSpec function, which takes the API folder and an OpenAPI definition object.

### What are the prerequisites for next-swagger-doc?

Nextjs >= 9 and Node >= 18. The package is published to npm as next-swagger-doc, is MIT licensed, and works as an ES module with a require entry at dist/index.cjs and an import entry at dist/index.js.

### What does the autoDoc option in next-swagger-doc do?

Setting autoDoc: true generates basic operations for App Router route.ts files. The path is derived from the route directory, dynamic segments such as [id] become {id}, and exported HTTP handlers such as GET and POST become operations with a default successful response. Manual @swagger blocks take precedence over generated ones.

### Why does next-swagger-doc produce an empty spec on a standalone build?

Because output: 'standalone' copies a minimal server without the source app/api files, so there is nothing to scan at runtime. The fix is to generate the document at build time with npx next-swagger-doc-cli next-swagger-doc.json --output public/swagger.json and load it with the specFile option.

### Why does next-swagger-doc avoid scanning the .next directory?

Walking .next while next build is compiling it can fail Vercel with ENOENT: .next/export-detail.json, so source API files and public OpenAPI files are scanned instead. The scanBuildOutput option should be set only when you intentionally want compiled .next/server files included.

## Sources

- [jellydn/next-swagger-doc on GitHub](https://github.com/jellydn/next-swagger-doc)
- [License: MIT](https://github.com/jellydn/next-swagger-doc/blob/main/LICENSE)
- [Project website](https://next-swagger-doc-demo.productsway.com/api-doc)
- [README](https://github.com/jellydn/next-swagger-doc/blob/main/README.md)
- [Releases](https://github.com/jellydn/next-swagger-doc/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/jellydn-next-swagger-doc
