@ntxdev/postman-generator
v0.1.0
Published
Generate Postman collections from NestJS Swagger + GraphQL schemas.
Downloads
67
Readme
@ntxdev/postman-generator
Self-contained, project-agnostic Postman collection generator for NestJS apps with REST (Swagger) and/or GraphQL.
Installation
Packages are public — no .npmrc or auth setup required.
npm install --save-dev @ntxdev/postman-generatorAdd these scripts to your project's package.json:
{
"postman:init": "npx postman-init",
"postman:swagger": "npx postman-extract-swagger",
"postman:generate": "npx postman-generate",
"postman:sync": "npx postman-sync",
"postman": "npm run postman:swagger && npm run postman:generate"
}Quick Start
# First-time setup (auto-detect project, no DB required)
npm run postman:init
# Sync folder config from resolver files (no DB required)
npm run postman:sync
# Full pipeline (requires DB connection for Swagger extraction)
npm run postman
# GraphQL only (no DB required, reads src/schema.gql)
npm run postman:generate -- --skip-rest
# REST only
npm run postman:generate -- --skip-gqlThe postman:init command creates a postman-generator/ directory in your project root with postman.config.json and examples.json. These files live in the consuming project, not inside the package.
Output lands in postman-generator/<collection-name>.postman_collection.json. Import it into Postman.
How It Works
src/schema.gql ──────────────► graphql-to-postman ─┐
├─► merge + enrich ─► .postman_collection.json
postman-generator/swagger.json ► openapi-to-postmanv2┘ ▲
│
postman-generator/postman.config.json
postman-generator/examples.json- Init:
npx postman-initscanspackage.json,src/main.ts, andsrc/app.module.tsto auto-detect project info (name, port, schema path, auth routes, Swagger metadata) and writes a minimal config. - GraphQL: The schema file (
src/schema.gql) is auto-generated by NestJS code-first. The generator reads it directly — no server required. - REST/Swagger:
npx postman-extract-swaggerreads the AppModule path and Swagger metadata from config, bootstraps the NestJS app without binding a port, and dumps the OpenAPI JSON. - Merge: The generator converts both artifacts into Postman items, applies example variables, and writes the final collection.
Project Layout
After running npx postman-init, your project will contain:
| File | Committed | Project-specific | Purpose |
| ------------------------------------------------- | --------- | ---------------- | ------------------------------------------------------------ |
| postman-generator/postman.config.json | Yes | Yes | Project config (collection name, auth, paths) |
| postman-generator/examples.json | Yes | Yes | GraphQL example variable overrides |
| postman-generator/swagger.json | No | Yes | Generated Swagger spec (build artifact) |
| postman-generator/*.postman_collection.json | No | Yes | Generated Postman collection (build artifact) |
Init — Auto-Detection
The npx postman-init command scans the project to generate a minimal config. It detects:
| Setting | Detection method |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------- |
| collectionName | package.json name field, title-cased |
| description | package.json description field |
| url | Port from configService.get('PORT', N) or .listen(N) in main.ts |
| graphql.schemaPath | autoSchemaFile in app.module.ts, or glob for *.gql files |
| graphql.endpointPath | GraphQLModule.forRoot config; defaults to /graphql |
| graphql.resolverGlobs | Scans for *.resolver.ts files and derives minimal glob patterns |
| swagger.appModulePath | Finds app.module.ts relative to project root |
| swagger.title/desc/ver | DocumentBuilder chain in main.ts |
| auth | @Controller('auth') + @Get('token/...') pattern; AuthGuard usage |
| variables | Seeds url, local_url, staging_url (placeholder), prod_url (placeholder), and authToken (if auth detected) |
If the config already exists, npx postman-init merges new detections without overwriting manual edits.
Syncing Folder Config
The npx postman-sync command scans resolver files for @Query and @Mutation decorators, extracts operation names, and writes the graphql.folders mapping in postman.config.json. This means you don't need to manually register every new resolver operation.
npm run postman:syncHow operations are detected
Two patterns are recognized:
- Explicit name —
@Query(() => Foo, { name: 'operationName' })— thenameproperty wins. - Method name —
@Query(() => Foo)\n async methodName(...)— method name is used when no explicit name is set.
How folder names are derived
The folder name comes from the resolver file name:
| File | Folder name |
| ----------------------------------------------- | ------------- |
| src/meta-v2/resolvers/campaign.resolver.ts | Campaign |
| src/meta-v2/resolvers/ad-group-ad.resolver.ts | Ad Group Ad |
| src/metamarketing/resolvers/ads.resolver.ts | Ads |
| src/metamarketing/metamarketing.resolver.ts | Metamarketing |
Overrides
Operations that live outside resolver files (e.g. hello in app.controller.ts) or that need manual reassignment use graphql.folderOverrides:
"folderOverrides": {
"hello": "Debugging",
"metaAccessTest": "Legacy",
"allMetaCampaigns": "Legacy"
}Overrides are applied after the auto-scan and always win. The sync script preserves overrides across runs.
Sub-folders (Queries / Mutations)
When graphql.subFolders is true (the default), each domain folder that contains both queries and mutations is split into Queries and Mutations sub-folders:
GraphQL / Campaigns
├── Queries
│ ├── campaigns
│ ├── campaign
│ └── campaignsByBudget
└── Mutations
├── createCampaign
├── updateCampaign
└── deleteCampaignDomains with only one type (e.g. a "Reports" folder with only queries) stay flat to avoid unnecessary nesting. Set graphql.subFolders to false to disable this entirely.
REST Controllers — Swagger Decorators
Swagger decorators on controllers and DTOs flow into the OpenAPI spec, which becomes the REST section of the Postman collection.
Controller-Level
import { Controller, Get, Post, Body, Param } from '@nestjs/common';
import {
ApiTags,
ApiOperation,
ApiResponse,
ApiParam,
} from '@nestjs/swagger';
@ApiTags('Leads') // <— folder name in Postman
@Controller('v2/leads')
export class LeadsController {
@Get(':id')
@ApiOperation({ summary: 'Get lead by ID' })
@ApiParam({ name: 'id', description: 'Lead UUID' })
@ApiResponse({
status: 200,
description: 'Lead found',
type: LeadDto,
})
getById(@Param('id') id: string) {
/* ... */
}
@Post()
@ApiOperation({ summary: 'Create a lead' })
@ApiResponse({ status: 201, type: LeadDto })
create(@Body() dto: CreateLeadDto) {
/* ... */
}
}DTO-Level
import { ApiProperty } from '@nestjs/swagger';
export class CreateLeadDto {
@ApiProperty({
description: 'Contact email',
example: '[email protected]',
})
email: string;
@ApiProperty({
description: 'Lead source',
example: 'facebook_form',
})
source: string;
}Decorator Cheat Sheet
| Decorator | Where | Purpose |
| --------------------------------------------- | ---------------- | ---------------------------- |
| @ApiTags('name') | Controller class | Groups into a Postman folder |
| @ApiOperation({ summary, description }) | Method | Request name + description |
| @ApiParam({ name, description, example }) | Method | URL path parameter docs |
| @ApiQuery({ name, description, example }) | Method | Query string parameter docs |
| @ApiBody({ type, description }) | Method | Request body docs |
| @ApiProperty({ description, example }) | DTO property | Field docs + example values |
| @ApiResponse({ status, description, type }) | Method | Response docs |
GraphQL Resolvers — Description Decorators
NestJS code-first generates src/schema.gql with descriptions from your decorators. The generator reads descriptions from that schema.
Resolver-Level
import { Query, Mutation, Args } from '@nestjs/graphql';
@Resolver()
export class CampaignResolver {
@Query(() => [Campaign], {
description: 'List all campaigns for a Meta ad account.',
})
campaigns(
@Args('accountId', {
type: () => String,
description: 'The ad account ID (without act_ prefix).',
})
accountId: string,
) {
/* ... */
}
@Mutation(() => Campaign, {
description: 'Create a Meta campaign. Defaults to PAUSED.',
})
createCampaign(
@Args('accountId', { description: 'Ad account ID.' })
accountId: string,
@Args('input', {
type: () => CreateCampaignInput,
description: 'New campaign fields.',
})
input: CreateCampaignInput,
) {
/* ... */
}
}Input/Object Types
import { InputType, Field } from '@nestjs/graphql';
@InputType({ description: 'Input for creating a Meta campaign.' })
export class CreateCampaignInput {
@Field(() => String, { description: 'Campaign name.' })
name: string;
@Field(() => String, {
description:
'Meta objective (e.g. OUTCOME_TRAFFIC, OUTCOME_LEADS).',
})
objective: string;
@Field(() => String, {
nullable: true,
description: 'Initial status. Defaults to PAUSED if omitted.',
})
status?: string;
}Key Points
- Always provide a
descriptionon@Query,@Mutation,@Field, and@Args. - Descriptions appear in the generated schema and in the Postman request docs.
- For example hints, embed them in the description:
'Ad account ID (e.g. "123456")'. - JSDoc comments (
/** ... */) are not reliably picked up when@Field(() => Type)is used. Stick to thedescriptionoption. @Extensions()is not used for examples (broken on@InputTypefields).
GraphQL Example Variables
The generator can overlay example variable values onto GraphQL requests so they are ready to use in Postman.
Edit postman-generator/examples.json. Keys are the schema field names (from type Query / type Mutation):
{
"campaigns": { "accountId": "123456" },
"createCampaign": {
"accountId": "123456",
"input": {
"name": "Test Campaign",
"objective": "OUTCOME_AWARENESS",
"status": "PAUSED"
}
}
}Operations without an entry get type-based placeholder values from the converter.
GraphQL Query Post-Processing
After graphql-to-postman emits raw items, the generator runs a pipeline of normalization passes to produce queries that are readable, executable, and ergonomic inside Postman. In pipeline order:
| Pass | Purpose | Config knob |
| ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------- |
| expandTruncatedQueries | The converter truncates any object-typed field at its depth limit. This pass walks each query, detects leaf object fields it left unexpanded (including fields with argument lists like feeds(pagination: ...)), and splices in a scalar-only selection so the request is still executable. | graphql.depth |
| applyDedupRepeatedTypes | Three sub-passes on the parsed query AST: (1) repeat-type dedup — on every second-or-later encounter of an object type, keep only scalar leaves (including FK ids) and drop nested expansions, collapsing the fan-out that depth-only expansion produces on cyclic schemas; (2) sibling-field dedup — collapse duplicate sibling Fields to the richer variant (has-args > more-sub-selections > first-seen); (3) per-field variable scoping (see below). | graphql.dedupRepeatedTypes (default true) |
| applyVariableDefaults | Exact-key override: for every key in variableDefaults that appears in a request's body.graphql.variables JSON, replace the value. | graphql.variableDefaults |
| applyGqlExamples | If examples.json has an entry for the operation, it replaces the entire variables JSON. Richest form of seeding — resolver-aware, wins over earlier defaults. | examplesPath |
| applyVariableDefaultsByType | For each declared operation variable, if its unwrapped input type is in variableDefaultsByType AND the key isn't already in the JSON body, seed it. Prefers inheriting from any existing sibling of the same type (so a rich $pagination example propagates to every $<field>_PaginationInput for consistency), falling back to the configured value. | graphql.variableDefaultsByType |
Per-field variable scoping
graphql-to-postman depth-expands cyclic schemas and emits one numbered variable per occurrence of the same argument ($pagination1, $pagination811, ..., $paginationN). The dedup pass rewrites these so:
Root operation fields keep the plain
$<argName>form:query dealersV2($pagination: PaginationInput) { dealersV2(pagination: $pagination) { ... } }Nested fields get
$<fieldName>_<InputTypeName>so every collection inside the response can be paginated independently in Postman:query dealersByResource( $feeds_PaginationInput: PaginationInput, $mastersheets_PaginationInput: PaginationInput, $budgets_PaginationInput: PaginationInput, $campaigns_PaginationInput: PaginationInput, $ad_groups_PaginationInput: PaginationInput, $ads_PaginationInput: PaginationInput, $resource_key: String!, $resource_values: [String!]! ) { dealersByResource(resource_key: $resource_key, resource_values: $resource_values) { feeds(pagination: $feeds_PaginationInput) { ... } mastersheets(pagination: $mastersheets_PaginationInput) { ... } ... } }
Multiple occurrences of the same nested field share one variable. If two proposed variables would collide on the same name but carry different type signatures, later ones get _2, _3, ... suffixes.
Type-based variable defaults
variableDefaultsByType seeds defaults keyed by GraphQL input type name (unwrapped, so PaginationInput! / [PaginationInput!] both match PaginationInput). Typical entry:
"variableDefaultsByType": {
"PaginationInput": { "take": 10, "skip": 0 }
}Every variable declared with that type — including all the per-field $<field>_PaginationInput introduced by the dedup pass — picks up the default without needing to list them by name.
Precedence for a given variable key is: explicit resolver example (examples.json) > inherited from an existing sibling of the same type > variableDefaultsByType[typeName] > converter-emitted placeholder.
Configuration
All project-specific settings live in postman-generator/postman.config.json:
| Key | Description |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| collectionName | Name shown in Postman |
| description | Collection-level description |
| url | Default {{url}} variable value |
| graphql.schemaPath | Path to the auto-generated .gql schema |
| graphql.endpointPath | GraphQL endpoint path (default /graphql) |
| graphql.depth | Query nesting depth for the converter (max 4) |
| graphql.resolverGlobs | Glob patterns for resolver files (default ["src/**/*.resolver.ts"]) |
| graphql.folderOverrides | Manual operation → folder assignments (applied after auto-scan) |
| graphql.subFolders | Nest Queries/Mutations within domain folders (default true) |
| graphql.folders | Auto-generated by npx postman-sync — operation → domain folder mapping |
| graphql.dedupRepeatedTypes | Run the repeat-type / sibling-field / per-field-variable normalization passes on generated queries (default true; see GraphQL Query Post-Processing). Set to false to keep raw converter output. |
| graphql.variableDefaults | Exact-key overrides applied to every request's body.graphql.variables — any matching top-level key is replaced with the configured value. |
| graphql.variableDefaultsByType | Seed defaults keyed by GraphQL input type name (unwrapped). Every operation variable of that type is defaulted when missing from the JSON body; values already seeded by a resolver example or a same-typed sibling win over this fallback. |
| swagger.specPath | Path to the generated Swagger JSON |
| swagger.appModulePath | Relative path to the AppModule file (from project root) |
| swagger.title | Swagger document title |
| swagger.description | Swagger document description |
| swagger.version | Swagger document version |
| auth | Auth config (bearer token, bootstrap request) |
| variables | Collection variable defaults |
| environments | Per-env variable overrides (e.g. { dev: { url }, staging: {...}, prod: {...} }); each entry generates a *.{env}.postman_environment.json selectable from Postman's environment dropdown |
| examplesPath | Path to the GQL examples JSON |
Adding a New Endpoint to the Collection
REST Controller
- Add Swagger decorators (
@ApiTags,@ApiOperation,@ApiProperty, etc.) to your controller and DTOs. - Run
npm run postman(ornpm run postman:swagger && npm run postman:generate). - The new endpoint appears automatically in the REST folder.
GraphQL Resolver
- Add
descriptionto your@Query/@Mutation/@Field/@Argsdecorators. - Optionally add example variables to
postman-generator/examples.json. - Run
npm run postman:syncto pick up the new operation and assign it to a folder. - Run
npm run postman:generate(ornpm run postmanfor the full pipeline). - The new operation appears automatically in the correct domain folder.
Troubleshooting
| Problem | Fix |
| -------------------------- | ------------------------------------------------------------------------------------ |
| Swagger extraction fails | Ensure DB is reachable; use --skip-rest for GQL-only |
| GraphQL operations missing | Run nest build first to regenerate schema.gql |
| Examples not applied | Check that the key in examples.json matches the schema field name exactly |
| Depth too shallow | Increase graphql.depth in config (max 4 via library, use CLI for deeper) |
| REST routes not appearing | Add @ApiTags to the controller class so Swagger discovers it |
| Config not found | Run npx postman-init to generate the initial postman-generator/postman.config.json |
