next-openapi-gen 0.6.2 → 0.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -75,6 +75,9 @@ During initialization (`npx next-openapi-gen init`), a configuration file `next.
75
75
  | `outputFile` | Path to the OpenAPI output file |
76
76
  | `docsUrl` | API documentation URL (for Swagger UI) |
77
77
  | `includeOpenApiRoutes` | Whether to include only routes with @openapi tag |
78
+ | `defaultResponseSet` | Default error response set for all endpoints |
79
+ | `responseSets` | Named sets of error response codes |
80
+ | `errorConfig` | Error schema configuration |
78
81
 
79
82
  ## Documenting Your API
80
83
 
@@ -154,6 +157,8 @@ export async function GET(
154
157
  | `@bodyDescription` | Request body description |
155
158
  | `@response` | Response type/schema |
156
159
  | `@responseDescription` | Response description |
160
+ | `@responseSet` | Override default response set (`public`, `auth`, `none`) |
161
+ | `@add` | Add custom response codes (`409:ConflictResponse`, `429`) |
157
162
  | `@contentType` | Request body content type (`application/json`, `multipart/form-data`) |
158
163
  | `@auth` | Authorization type (`bearer`, `basic`, `apikey`) |
159
164
  | `@tag` | Custom tag |
@@ -373,6 +378,89 @@ export async function POST() {
373
378
  }
374
379
  ```
375
380
 
381
+ ## Response Management
382
+
383
+ ### Zero Config + Response Sets
384
+
385
+ Configure reusable error sets in `next.openapi.json`:
386
+
387
+ ```json
388
+ {
389
+ "defaultResponseSet": "common",
390
+ "responseSets": {
391
+ "common": ["400", "401", "500"],
392
+ "public": ["400", "500"],
393
+ "auth": ["400", "401", "403", "500"]
394
+ }
395
+ }
396
+ ```
397
+
398
+ ### Usage Examples
399
+
400
+ ```typescript
401
+ /**
402
+ * Auto-default responses
403
+ * @response UserResponse
404
+ * @openapi
405
+ */
406
+ export async function GET() {}
407
+ // Generates: 200:UserResponse + common errors (400, 401, 500)
408
+
409
+ /**
410
+ * Override response set
411
+ * @response ProductResponse
412
+ * @responseSet public
413
+ * @openapi
414
+ */
415
+ export async function GET() {}
416
+ // Generates: 200:ProductResponse + public errors (400, 500)
417
+
418
+ /**
419
+ * Add custom responses
420
+ * @response 201:UserResponse
421
+ * @add 409:ConflictResponse
422
+ * @openapi
423
+ */
424
+ export async function POST() {}
425
+ // Generates: 201:UserResponse + common errors + 409:ConflictResponse
426
+
427
+ /**
428
+ * Combine multiple sets
429
+ * @response UserResponse
430
+ * @responseSet auth,crud
431
+ * @add 429:RateLimitResponse
432
+ * @openapi
433
+ */
434
+ export async function PUT() {}
435
+ // Combines: auth + crud errors + custom 429
436
+ ```
437
+
438
+ ### Error Schema Configuration
439
+
440
+ #### Define consistent error schemas using templates:
441
+
442
+ ```json
443
+ {
444
+ "errorConfig": {
445
+ "template": {
446
+ "type": "object",
447
+ "properties": {
448
+ "error": {
449
+ "type": "string",
450
+ "example": "{{ERROR_MESSAGE}}"
451
+ }
452
+ }
453
+ },
454
+ "codes": {
455
+ "invalid_request": {
456
+ "description": "Invalid request",
457
+ "variables": { "ERROR_MESSAGE": "Validation failed" }
458
+ }
459
+ }
460
+ }
461
+ }
462
+ ```
463
+
376
464
  ## Advanced Usage
377
465
 
378
466
  ### Automatic Path Parameter Detection
@@ -14,7 +14,7 @@ export class OpenApiGenerator {
14
14
  }
15
15
  getConfig() {
16
16
  // @ts-ignore
17
- const { apiDir, schemaDir, docsUrl, ui, outputFile, includeOpenApiRoutes, schemaType = "typescript" } = this.template;
17
+ const { apiDir, schemaDir, docsUrl, ui, outputFile, includeOpenApiRoutes, schemaType = "typescript", defaultResponseSet, responseSets, errorConfig } = this.template;
18
18
  return {
19
19
  apiDir,
20
20
  schemaDir,
@@ -23,6 +23,9 @@ export class OpenApiGenerator {
23
23
  outputFile,
24
24
  includeOpenApiRoutes,
25
25
  schemaType,
26
+ defaultResponseSet,
27
+ responseSets,
28
+ errorConfig,
26
29
  };
27
30
  }
28
31
  generate() {
@@ -57,6 +60,21 @@ export class OpenApiGenerator {
57
60
  if (!this.template.components.schemas) {
58
61
  this.template.components.schemas = {};
59
62
  }
63
+ // Generate error responses using errorConfig or manual definitions
64
+ if (!this.template.components.responses) {
65
+ this.template.components.responses = {};
66
+ }
67
+ const errorConfig = this.config.errorConfig;
68
+ if (errorConfig) {
69
+ this.generateErrorResponsesFromConfig(errorConfig);
70
+ }
71
+ else if (this.config.errorDefinitions) {
72
+ // Use manual definitions (existing logic - if exists)
73
+ Object.entries(this.config.errorDefinitions).forEach(([code, errorDef]) => {
74
+ this.template.components.responses[code] =
75
+ this.createErrorResponseComponent(code, errorDef);
76
+ });
77
+ }
60
78
  // Get defined schemas from the processor
61
79
  const definedSchemas = this.routeProcessor
62
80
  .getSchemaProcessor()
@@ -70,4 +88,70 @@ export class OpenApiGenerator {
70
88
  const openapiSpec = cleanSpec(this.template);
71
89
  return openapiSpec;
72
90
  }
91
+ generateErrorResponsesFromConfig(errorConfig) {
92
+ const { template, codes, variables: globalVars = {} } = errorConfig;
93
+ Object.entries(codes).forEach(([errorCode, config]) => {
94
+ const httpStatus = (config.httpStatus || this.guessHttpStatus(errorCode)).toString();
95
+ // Merge variables: global + per-code + built-in
96
+ const allVariables = {
97
+ ...globalVars,
98
+ ...config.variables,
99
+ ERROR_CODE: errorCode,
100
+ DESCRIPTION: config.description,
101
+ HTTP_STATUS: httpStatus,
102
+ };
103
+ const processedSchema = this.processTemplate(template, allVariables);
104
+ this.template.components.responses[httpStatus] = {
105
+ description: config.description,
106
+ content: {
107
+ "application/json": {
108
+ schema: processedSchema,
109
+ },
110
+ },
111
+ };
112
+ });
113
+ }
114
+ processTemplate(template, variables) {
115
+ const jsonStr = JSON.stringify(template);
116
+ let result = jsonStr;
117
+ Object.entries(variables).forEach(([key, value]) => {
118
+ result = result.replace(new RegExp(`{{${key}}}`, "g"), value);
119
+ });
120
+ return JSON.parse(result);
121
+ }
122
+ guessHttpStatus(errorCode) {
123
+ const statusMap = {
124
+ bad: 400,
125
+ invalid: 400,
126
+ validation: 422,
127
+ unauthorized: 401,
128
+ auth: 401,
129
+ forbidden: 403,
130
+ permission: 403,
131
+ not_found: 404,
132
+ missing: 404,
133
+ conflict: 409,
134
+ duplicate: 409,
135
+ rate_limit: 429,
136
+ too_many: 429,
137
+ server: 500,
138
+ internal: 500,
139
+ };
140
+ for (const [key, status] of Object.entries(statusMap)) {
141
+ if (errorCode.toLowerCase().includes(key)) {
142
+ return status;
143
+ }
144
+ }
145
+ return 500;
146
+ }
147
+ createErrorResponseComponent(code, errorDef) {
148
+ return {
149
+ description: errorDef.description,
150
+ content: {
151
+ "application/json": {
152
+ schema: errorDef.schema,
153
+ },
154
+ },
155
+ };
156
+ }
73
157
  }
@@ -18,6 +18,91 @@ export class RouteProcessor {
18
18
  this.config = config;
19
19
  this.schemaProcessor = new SchemaProcessor(config.schemaDir, config.schemaType);
20
20
  }
21
+ buildResponsesFromConfig(dataTypes, method) {
22
+ const responses = {};
23
+ // 1. Add success response
24
+ const successCode = dataTypes.successCode || this.getDefaultSuccessCode(method);
25
+ if (dataTypes.responseType) {
26
+ const responseSchema = this.schemaProcessor.getSchemaContent({
27
+ responseType: dataTypes.responseType,
28
+ }).responses;
29
+ responses[successCode] = {
30
+ description: dataTypes.responseDescription || "Successful response",
31
+ content: {
32
+ "application/json": {
33
+ schema: responseSchema,
34
+ },
35
+ },
36
+ };
37
+ }
38
+ // 2. Add responses from ResponseSet
39
+ const responseSetName = dataTypes.responseSet || this.config.defaultResponseSet;
40
+ if (responseSetName && responseSetName !== "none") {
41
+ const responseSets = this.config.responseSets || {};
42
+ const setNames = responseSetName.split(",").map((s) => s.trim());
43
+ setNames.forEach((setName) => {
44
+ const responseSet = responseSets[setName];
45
+ if (responseSet) {
46
+ responseSet.forEach((errorCode) => {
47
+ // Use $ref for components/responses
48
+ responses[errorCode] = {
49
+ $ref: `#/components/responses/${errorCode}`,
50
+ };
51
+ });
52
+ }
53
+ });
54
+ }
55
+ // 3. Add custom responses (@add)
56
+ if (dataTypes.addResponses) {
57
+ const customResponses = dataTypes.addResponses
58
+ .split(",")
59
+ .map((s) => s.trim());
60
+ customResponses.forEach((responseRef) => {
61
+ const [code, ref] = responseRef.split(":");
62
+ if (ref) {
63
+ // Custom schema: "409:ConflictResponse"
64
+ responses[code] = {
65
+ description: this.getDefaultErrorDescription(code) || `HTTP ${code} response`,
66
+ content: {
67
+ "application/json": {
68
+ schema: { $ref: `#/components/schemas/${ref}` },
69
+ },
70
+ },
71
+ };
72
+ }
73
+ else {
74
+ // Only code: "409" - use $ref fro components/responses
75
+ responses[code] = {
76
+ $ref: `#/components/responses/${code}`,
77
+ };
78
+ }
79
+ });
80
+ }
81
+ return responses;
82
+ }
83
+ getDefaultSuccessCode(method) {
84
+ switch (method.toUpperCase()) {
85
+ case "POST":
86
+ return "201";
87
+ case "DELETE":
88
+ return "204";
89
+ default:
90
+ return "200";
91
+ }
92
+ }
93
+ getDefaultErrorDescription(code) {
94
+ const defaults = {
95
+ 400: "Bad Request",
96
+ 401: "Unauthorized",
97
+ 403: "Forbidden",
98
+ 404: "Not Found",
99
+ 409: "Conflict",
100
+ 422: "Unprocessable Entity",
101
+ 429: "Too Many Requests",
102
+ 500: "Internal Server Error",
103
+ };
104
+ return defaults[code] || `HTTP ${code}`;
105
+ }
21
106
  /**
22
107
  * Get the SchemaProcessor instance
23
108
  */
@@ -163,9 +248,13 @@ export class RouteProcessor {
163
248
  definition.requestBody = this.schemaProcessor.createRequestBodySchema(body, bodyDescription, dataTypes.contentType);
164
249
  }
165
250
  // Add responses
166
- definition.responses = responses
167
- ? this.schemaProcessor.createResponseSchema(responses, responseDescription)
168
- : {};
251
+ definition.responses = this.buildResponsesFromConfig(dataTypes, method);
252
+ // If there are no responses from config, use the old logic
253
+ if (Object.keys(definition.responses).length === 0) {
254
+ definition.responses = responses
255
+ ? this.schemaProcessor.createResponseSchema(responses, responseDescription)
256
+ : {};
257
+ }
169
258
  this.swaggerPaths[routePath][method] = definition;
170
259
  }
171
260
  getRoutePath(filePath) {
@@ -514,6 +514,26 @@ export class SchemaProcessor {
514
514
  }
515
515
  return "application/json";
516
516
  }
517
+ createMultipleResponsesSchema(responses, defaultDescription) {
518
+ const result = {};
519
+ Object.entries(responses).forEach(([code, response]) => {
520
+ if (typeof response === "string") {
521
+ // Reference do components/responses
522
+ result[code] = { $ref: `#/components/responses/${response}` };
523
+ }
524
+ else {
525
+ result[code] = {
526
+ description: response.description || defaultDescription || "Response",
527
+ content: {
528
+ "application/json": {
529
+ schema: response.schema || response,
530
+ },
531
+ },
532
+ };
533
+ }
534
+ });
535
+ return result;
536
+ }
517
537
  createFormDataSchema(body) {
518
538
  if (!body.properties) {
519
539
  return body;
package/dist/lib/utils.js CHANGED
@@ -22,13 +22,16 @@ export function extractJSDocComments(path) {
22
22
  let paramsType = "";
23
23
  let pathParamsType = "";
24
24
  let bodyType = "";
25
- let responseType = "";
26
25
  let auth = "";
27
26
  let isOpenApi = false;
28
27
  let deprecated = false;
29
28
  let bodyDescription = "";
30
- let responseDescription = "";
31
29
  let contentType = "";
30
+ let responseType = "";
31
+ let responseDescription = "";
32
+ let responseSet = "";
33
+ let addResponses = "";
34
+ let successCode = "";
32
35
  if (comments) {
33
36
  comments.forEach((comment) => {
34
37
  const commentValue = cleanComment(comment.value);
@@ -43,13 +46,6 @@ export function extractJSDocComments(path) {
43
46
  bodyDescription = match[1].trim();
44
47
  }
45
48
  }
46
- if (commentValue.includes("@responseDescription")) {
47
- const regex = /@responseDescription\s*(.*)/;
48
- const match = commentValue.match(regex);
49
- if (match && match[1]) {
50
- responseDescription = match[1].trim();
51
- }
52
- }
53
49
  if (!summary) {
54
50
  summary = commentValue.split("\n")[0];
55
51
  }
@@ -98,6 +94,38 @@ export function extractJSDocComments(path) {
98
94
  contentType = match[1].trim();
99
95
  }
100
96
  }
97
+ if (commentValue.includes("@responseDescription")) {
98
+ const regex = /@responseDescription\s*(.*)/;
99
+ const match = commentValue.match(regex);
100
+ if (match && match[1]) {
101
+ responseDescription = match[1].trim();
102
+ }
103
+ }
104
+ if (commentValue.includes("@responseSet")) {
105
+ const regex = /@responseSet\s*(.*)/;
106
+ const match = commentValue.match(regex);
107
+ if (match && match[1]) {
108
+ responseSet = match[1].trim();
109
+ }
110
+ }
111
+ if (commentValue.includes("@add")) {
112
+ const regex = /@add\s*(.*)/;
113
+ const match = commentValue.match(regex);
114
+ if (match && match[1]) {
115
+ addResponses = match[1].trim();
116
+ }
117
+ }
118
+ if (commentValue.includes("@response")) {
119
+ const responseMatch = commentValue.match(/@response\s+(?:(\d+):)?(\w+)(?:\s+(.*))?/);
120
+ if (responseMatch) {
121
+ const [, code, type] = responseMatch;
122
+ successCode = code || "";
123
+ responseType = type;
124
+ }
125
+ else {
126
+ responseType = extractTypeFromComment(commentValue, "@response");
127
+ }
128
+ }
101
129
  });
102
130
  }
103
131
  return {
@@ -108,12 +136,15 @@ export function extractJSDocComments(path) {
108
136
  paramsType,
109
137
  pathParamsType,
110
138
  bodyType,
111
- responseType,
112
139
  isOpenApi,
113
140
  deprecated,
114
141
  bodyDescription,
115
- responseDescription,
116
142
  contentType,
143
+ responseType,
144
+ responseDescription,
145
+ responseSet,
146
+ addResponses,
147
+ successCode,
117
148
  };
118
149
  }
119
150
  export function extractTypeFromComment(commentValue, tag) {
@@ -130,6 +161,10 @@ export function cleanSpec(spec) {
130
161
  "ui",
131
162
  "outputFile",
132
163
  "includeOpenApiRoutes",
164
+ "schemaType",
165
+ "defaultResponseSet",
166
+ "responseSets",
167
+ "errorConfig",
133
168
  ];
134
169
  const newSpec = { ...spec };
135
170
  propsToRemove.forEach((key) => delete newSpec[key]);
@@ -20,6 +20,49 @@ export default {
20
20
  },
21
21
  },
22
22
  },
23
+ defaultResponseSet: "common",
24
+ responseSets: {
25
+ common: ["400", "500"],
26
+ auth: ["401"],
27
+ },
28
+ errorConfig: {
29
+ template: {
30
+ type: "object",
31
+ properties: {
32
+ success: {
33
+ type: "boolean",
34
+ example: false,
35
+ },
36
+ error: {
37
+ type: "string",
38
+ example: "{{ERROR_MESSAGE}}",
39
+ },
40
+ },
41
+ },
42
+ codes: {
43
+ invalid: {
44
+ description: "Bad request",
45
+ httpStatus: 400,
46
+ variables: {
47
+ ERROR_MESSAGE: "Validation error",
48
+ },
49
+ },
50
+ auth: {
51
+ description: "Unauthorized",
52
+ httpStatus: 401,
53
+ variables: {
54
+ ERROR_MESSAGE: "Unathorized",
55
+ },
56
+ },
57
+ server_error: {
58
+ description: "Internal server error",
59
+ httpStatus: 500,
60
+ variables: {
61
+ ERROR_MESSAGE: "Something went wrong",
62
+ },
63
+ },
64
+ },
65
+ },
23
66
  apiDir: "./src/app/api",
24
67
  schemaDir: "./src",
25
68
  schemaType: "typescript",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "next-openapi-gen",
3
- "version": "0.6.2",
3
+ "version": "0.6.4",
4
4
  "description": "Automatically generate OpenAPI 3.0 documentation from Next.js projects, with support for TypeScript types and Zod schemas.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",