Skip to content

feat: Add automatic Postman collection generation and download endpoint Closes #199#356

Open
mich-sys wants to merge 1 commit intoEDOHWARES:mainfrom
mich-sys:feat/swagger-postman-export
Open

feat: Add automatic Postman collection generation and download endpoint Closes #199#356
mich-sys wants to merge 1 commit intoEDOHWARES:mainfrom
mich-sys:feat/swagger-postman-export

Conversation

@mich-sys
Copy link
Copy Markdown

Closes #199

Copilot AI review requested due to automatic review settings April 27, 2026 08:23
@drips-wave
Copy link
Copy Markdown

drips-wave Bot commented Apr 27, 2026

@mich-sys Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds automated Postman collection generation from the backend’s Swagger/OpenAPI (swagger-jsdoc) spec and exposes it via a download endpoint to improve third-party API testing (Issue #199).

Changes:

  • Add openapi-to-postmanv2 dependency to the backend workspace.
  • Generate a cached Postman collection at server start from the existing swaggerSpec.
  • Expose /api/docs/postman.json to download the generated collection.

Reviewed changes

Copilot reviewed 2 out of 3 changed files in this pull request and generated 3 comments.

File Description
package-lock.json Updates workspace lockfile to include openapi-to-postmanv2 and its transitive dependencies.
backend/src/routes/docs.routes.js Generates Postman collection from swaggerSpec at startup and adds the /postman.json download route.
backend/package.json Adds openapi-to-postmanv2 to backend dependencies.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +162 to +165
if (err) {
console.error('Error generating Postman collection:', err);
} else if (!conversionResult.result) {
console.error('Could not convert to Postman format:', conversionResult.reason);
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

docs.routes.js uses console.error/console.log for runtime logging, but the backend consistently uses the shared config/logger (e.g., routes like src/routes/queue.routes.js). Please switch these to logger.error/logger.info (and import the logger) so logs are structured/consistent across environments.

Copilot uses AI. Check for mistakes.
Comment on lines +157 to +190

Converter.convert(
{ type: 'json', data: swaggerSpec },
{ folderStrategy: 'Tags', includeWebhooks: true },
(err, conversionResult) => {
if (err) {
console.error('Error generating Postman collection:', err);
} else if (!conversionResult.result) {
console.error('Could not convert to Postman format:', conversionResult.reason);
} else {
postmanCollection = conversionResult.output[0].data;
// Ensure pre-defined environment variables are included
postmanCollection.variable = postmanCollection.variable || [];

const hasAuth = postmanCollection.variable.find(v => v.key === 'AUTH_TOKEN');
if (!hasAuth) {
postmanCollection.variable.push({
key: 'AUTH_TOKEN',
value: '',
type: 'string',
description: 'Bearer token for authenticated requests'
});
}
console.log('Postman collection successfully generated from Swagger annotations.');
}
}
);

router.get('/postman.json', (req, res) => {
if (!postmanCollection) {
return res.status(503).json({ error: 'Postman collection is still generating or failed to generate.' });
}
res.attachment('EventHorizon_Postman_Collection.json');
res.json(postmanCollection);
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If Postman generation fails, the server only logs the error and leaves postmanCollection as null, so /postman.json will return a generic 503 forever with no way to recover or diagnose via the API. Consider storing the failure reason separately (e.g., last error + timestamp) and returning a 500 with a safe error message, and/or allowing regeneration (retry on next request or via an admin-only refresh endpoint).

Suggested change
Converter.convert(
{ type: 'json', data: swaggerSpec },
{ folderStrategy: 'Tags', includeWebhooks: true },
(err, conversionResult) => {
if (err) {
console.error('Error generating Postman collection:', err);
} else if (!conversionResult.result) {
console.error('Could not convert to Postman format:', conversionResult.reason);
} else {
postmanCollection = conversionResult.output[0].data;
// Ensure pre-defined environment variables are included
postmanCollection.variable = postmanCollection.variable || [];
const hasAuth = postmanCollection.variable.find(v => v.key === 'AUTH_TOKEN');
if (!hasAuth) {
postmanCollection.variable.push({
key: 'AUTH_TOKEN',
value: '',
type: 'string',
description: 'Bearer token for authenticated requests'
});
}
console.log('Postman collection successfully generated from Swagger annotations.');
}
}
);
router.get('/postman.json', (req, res) => {
if (!postmanCollection) {
return res.status(503).json({ error: 'Postman collection is still generating or failed to generate.' });
}
res.attachment('EventHorizon_Postman_Collection.json');
res.json(postmanCollection);
let isGeneratingPostmanCollection = false;
let lastPostmanGenerationError = null;
let lastPostmanGenerationErrorAt = null;
function generatePostmanCollection() {
if (isGeneratingPostmanCollection) {
return;
}
isGeneratingPostmanCollection = true;
lastPostmanGenerationError = null;
lastPostmanGenerationErrorAt = null;
Converter.convert(
{ type: 'json', data: swaggerSpec },
{ folderStrategy: 'Tags', includeWebhooks: true },
(err, conversionResult) => {
isGeneratingPostmanCollection = false;
if (err) {
postmanCollection = null;
lastPostmanGenerationError = 'Failed to generate Postman collection.';
lastPostmanGenerationErrorAt = new Date().toISOString();
console.error('Error generating Postman collection:', err);
} else if (!conversionResult.result) {
postmanCollection = null;
lastPostmanGenerationError = 'Failed to convert OpenAPI spec to Postman collection.';
lastPostmanGenerationErrorAt = new Date().toISOString();
console.error('Could not convert to Postman format:', conversionResult.reason);
} else {
postmanCollection = conversionResult.output[0].data;
lastPostmanGenerationError = null;
lastPostmanGenerationErrorAt = null;
// Ensure pre-defined environment variables are included
postmanCollection.variable = postmanCollection.variable || [];
const hasAuth = postmanCollection.variable.find(v => v.key === 'AUTH_TOKEN');
if (!hasAuth) {
postmanCollection.variable.push({
key: 'AUTH_TOKEN',
value: '',
type: 'string',
description: 'Bearer token for authenticated requests'
});
}
console.log('Postman collection successfully generated from Swagger annotations.');
}
}
);
}
generatePostmanCollection();
router.get('/postman.json', (req, res) => {
if (postmanCollection) {
res.attachment('EventHorizon_Postman_Collection.json');
return res.json(postmanCollection);
}
if (isGeneratingPostmanCollection) {
return res.status(503).json({ error: 'Postman collection is still generating.' });
}
if (lastPostmanGenerationError) {
generatePostmanCollection();
if (postmanCollection) {
res.attachment('EventHorizon_Postman_Collection.json');
return res.json(postmanCollection);
}
if (isGeneratingPostmanCollection) {
return res.status(503).json({
error: 'Postman collection regeneration is in progress.',
lastFailure: {
message: lastPostmanGenerationError,
timestamp: lastPostmanGenerationErrorAt,
},
});
}
return res.status(500).json({
error: 'Postman collection is unavailable due to a generation error.',
lastFailure: {
message: lastPostmanGenerationError,
timestamp: lastPostmanGenerationErrorAt,
},
});
}
generatePostmanCollection();
return res.status(503).json({ error: 'Postman collection is still generating.' });

Copilot uses AI. Check for mistakes.
Comment on lines +168 to +178
// Ensure pre-defined environment variables are included
postmanCollection.variable = postmanCollection.variable || [];

const hasAuth = postmanCollection.variable.find(v => v.key === 'AUTH_TOKEN');
if (!hasAuth) {
postmanCollection.variable.push({
key: 'AUTH_TOKEN',
value: '',
type: 'string',
description: 'Bearer token for authenticated requests'
});
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The collection variable AUTH_TOKEN is added, but the generated requests likely won’t automatically use it because the OpenAPI definition in this file doesn’t declare a bearer securityScheme (and the route JSDoc blocks generally don’t specify security). To make the exported Postman collection actually usable for authenticated endpoints, add a components.securitySchemes bearer auth definition to swaggerSpec.definition and apply it globally or per protected route so the converter generates Authorization headers that reference the token variable.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Backend: Swagger to Postman/Insomnia Automatic Export

2 participants