Sync Fastify route schemas to Postman automatically.
You keep writing normal Fastify routes with JSON Schema. This plugin reads the OpenAPI spec from @fastify/swagger, converts it with openapi-to-postmanv2, merges changes into an existing Postman collection (preserving your tests and scripts), and pushes updates through the Postman API so Postman Desktop picks them up via cloud sync.
-
Long OpenAPI
summarytext is great for documentation but a poor default for Postman request titles. pman usesx-pman-name/x-name(OpenAPI operation extensions) for short item names and still puts the fullsummaryin the request description (“Docs”). -
Monolithic or misleading folder layout when grouping only by a single path segment (or by tags) makes large APIs hard to browse. With
folderStrategy: 'path'(or'hybrid') andpathFolderNesting: 'nested'(default), URL prefixes become nested Postman folders—for examplePOST /auth/user/admin/createis grouped as Auth → User → Admin, with the request under Admin. -
Stale or duplicate folders after you change strategy or upgrade pman. The sync state tracks managed trees with path keys (e.g.
Auth>User>Admin) and, on each merge, derives extra removal keys from the current routes (e.g. first path segment, first tag) so older layouts (tag-only top-level folders, flat path folders from earlier releases) are still removed when you sync.
- Node.js 20+
- Fastify 5
- Register
@fastify/swaggerbefore this plugin sofastify.swagger()is available.
npm install @st3ix/pman @fastify/swaggerimport Fastify from 'fastify';
import swagger from '@fastify/swagger';
import pman from '@st3ix/pman';
const app = Fastify();
await app.register(swagger, {
openapi: {
openapi: '3.0.0',
info: { title: 'My API', version: '1.0.0' },
servers: [{ url: 'http://127.0.0.1:3000' }],
},
});
app.get('/users', {
schema: {
// Short Postman item title. Use an OpenAPI extension field because
// plain `name` is not emitted into OpenAPI by @fastify/swagger.
'x-pman-name': 'List users',
tags: ['Users'],
summary: 'List users in the current workspace',
response: { 200: { type: 'array' } },
},
}, async () => []);
await app.register(pman, {
postmanApiKey: 'PMAK-…',
// Either pass workspaceId directly...
workspaceId: '00000000-0000-4000-8000-000000000000',
// ...or pass a workspace link and let pman extract the id:
// workspaceLink: 'https://<team>.postman.co/workspace/My~00000000-0000-4000-8000-000000000000/overview',
postmanBaseUrl: 'http://127.0.0.1:3000',
collectionName: 'My API',
folderStrategy: 'path',
// Default: one Postman subfolder per path prefix segment, e.g. /auth/.../... → Auth → …
// Use 'flat' to only use the first segment as a single folder (legacy style).
pathFolderNesting: 'nested',
// Optional explicit auth config:
// auth: { type: 'apiKey', headerKey: 'X-API-Token', variableKey: 'apiToken' },
});
await app.listen({ port: 3000 });OpenAPI summary strings are often long, but they make poor Postman request titles. Set a short OpenAPI extension on the operation to control the Postman item name:
schema['x-pman-name'](recommended)schema['x-name'](also supported)
Use summary for the first paragraph of the generated Postman “Docs” text.
Postman stores request documentation on the request (item.request.description) in Collection v2.1; pman writes the same text to item.description as well for compatibility.
| Route schema field | How it is used in Postman |
|---|---|
x-pman-name / x-name |
Request title (short) |
summary |
First paragraph in the item description, followed by auto-generated route metadata |
If x-pman-name / x-name is omitted, the title falls back to METHOD <lastPathSegment> (for example GET users).
Pass postmanApiKey and workspaceId in the same object as the rest of the plugin options (recommended for apps you control). If either value is omitted or an empty string, the plugin falls back to POSTMAN_API_KEY / POSTMAN_WORKSPACE_ID.
| Route schema field | How it is used in Postman |
|---|---|
x-pman-name (recommended) or x-name |
Request title (short) |
summary |
First part of the item description (Docs), plus auto-generated route metadata |
If x-pman-name / x-name is omitted, the title falls back to something like GET users (method plus last path segment).
Postman stores primary documentation on the request (item.request.description); pman also writes the same text to item.description for compatibility with tools that only read the folder-level field.
folderStrategy |
Behaviour |
|---|---|
path (default) |
Folders follow the route URL (after optional folderPathStripPrefix). |
tags |
One folder per first OpenAPI tag (or Untagged). |
hybrid |
Tag when present; otherwise same as path for that operation. |
Path nesting applies when folderStrategy is path or hybrid and pathFolderNesting is set:
pathFolderNesting |
Behaviour |
|---|---|
nested (default) |
Each path prefix segment (except the last) becomes a folder, title-cased. Example: POST /auth/user/admin/create → folders Auth → User → Admin, request under Admin. |
flat |
Only the first segment (or the tag/hybrid resolution) is used as a single folder name—closer to older single-folder behaviour. |
Use folderPathStripPrefix to ignore a common API base (e.g. /v1) before computing segments.
Environment variables (optional fallback)
| Variable | Purpose |
|---|---|
POSTMAN_API_KEY |
Used when postmanApiKey is not set or is blank |
POSTMAN_WORKSPACE_ID |
Used when workspaceId is not set or is blank |
POSTMAN_BASE_URL |
Used when postmanBaseUrl is not set or is blank |
Pass postmanApiKey and workspaceId in the same object as the rest of the plugin options (recommended for apps you control). If either value is omitted or is an empty string, the plugin falls back to the environment variables above.
postmanBaseUrl defines the Postman collection variable baseUrl, so requests that use {{baseUrl}} resolve correctly. If you omit it (and POSTMAN_BASE_URL), the first OpenAPI servers[].url is used.
reuseExistingCollectionByName (default true): if there is no .postman-sync.json yet, the workspace is searched for a collection whose name equals collectionName; that collection is reused instead of creating a duplicate. Set to false to always create a new collection when no state file exists.
On onReady, the plugin reads the OpenAPI document, converts it, merges it into the Postman collection, and pushes changes. The first successful run creates a collection and stores its uid in .postman-sync.json (override with statePath).
Secrets are never written to the sync state file.
| Option | Description |
|---|---|
workspaceId |
Postman workspace id |
workspaceLink |
Postman workspace link (extracts workspaceId automatically) |
postmanApiKey |
Postman API key |
postmanBaseUrl |
Value for Postman variable baseUrl ({{baseUrl}} in URLs) |
reuseExistingCollectionByName |
Reuse workspace collection with same name when no state file (default true) |
collectionName |
Collection title (default Fastify (pman)) |
statePath |
Path to JSON state file (default .postman-sync.json in cwd) |
dryRun |
If true, no Postman HTTP calls are made |
folderStrategy |
path (default), tags, or hybrid — how routes are grouped into Postman folders |
folderPathStripPrefix |
Strip this URL prefix (normalized like OpenAPI) before path segments are used for folders (path / hybrid) |
pathFolderNesting |
nested (default) or flat — see Folder layout in Postman |
updateCheck |
If true (default), check npm for a newer @st3ix/pman version on startup and log an update hint |
postmanApiBase |
Override Postman API base URL |
fetchImpl |
Custom fetch (for tests) |
If the API key and workspace id are both missing (options and env), the plugin logs a warning and skips sync so local development still works.
If .postman-sync.json points to a collection that was deleted in Postman (HTTP 404), the plugin removes that state file, then tries reuse by name again or creates a new collection so a stale collection uid does not hard-fail the app.
- Requests managed by this plugin are tagged with
_pman.routeId. On each sync, spec-driven fields are refreshed from OpenAPI whileevent(tests, prerequest) is copied from the previous collection item when present. responsesaved examples are kept only if that request already had a non-emptyresponsearray in Postman; otherwise OpenAPI-generated example responses are not written (avoids clutter from the converter defaults).- The sync state file stores
managedFolders: one path key per managed route group (e.g.Auth>User>Admin) so the correct nested folder tree can be replaced on the next run even if many folders share a short name in different areas of the tree. - In addition, each merge computes removal aliases from the current route list (full path key, first URL segment, first tag) so legacy collection shapes from earlier pman versions (for example a single top-level
Userstag folder) are still removed after you switch to path-based nested folders or upgrade the plugin.
- Fastify: routes and schemas
@fastify/swagger: generates OpenAPIopenapi-to-postmanv2: converts OpenAPI to Postman Collection v2.1- Postman API: stores the collection in your workspace
- From the repo root: copy
.env.exampleto.envand setPOSTMAN_API_KEY,POSTMAN_WORKSPACE_ID, and optionallyPOSTMAN_BASE_URL(.envis gitignored), then runnpm run dev:example— builds the plugin, loads.env, picks a free port ifPORTis busy, syncs ononReady. - From
examples/: afternpm run build, editexamples/playground.mjsand fill thepostmanobject (postmanApiKey,workspaceId), then runnode --env-file=.env examples/playground.mjsornode examples/playground.mjs. The script picks 3030 (orPORT) or a free port, setspostmanBaseUrlto that origin, and usespino-prettyfor readable logs when the dev dependency is installed. The example usespathFolderNesting: 'nested'and includesPOST /auth/user/admin/createto verify Auth → User → Admin in Postman.
See CONTRIBUTING.md.
The package ships a small pman CLI:
pman help
pman clear
pman supportpman support prints the Discord invite: https://discord.gg/4FBYAMxwdk.
MIT