Skip to content

feat: Add OpenAPI/Swagger documentation#193

Open
kydrahul wants to merge 5 commits into
nexoraorg:mainfrom
kydrahul:main
Open

feat: Add OpenAPI/Swagger documentation#193
kydrahul wants to merge 5 commits into
nexoraorg:mainfrom
kydrahul:main

Conversation

@kydrahul

@kydrahul kydrahul commented Jun 16, 2026

Copy link
Copy Markdown

Closes #172

Changes

  • Swagger UI at /api-docs
  • JSON spec at /api-docs.json, YAML at /api-docs.yaml
  • OpenAPI schemas for Auth, Accounts, Credit Scoring, Fraud Detection, Health
  • Request validation middleware (swaggerValidator.ts)
  • schemas.yaml reference file for SDK generators
  • generate-types.ts script for TypeScript type generation

Screenshot

image

Summary by CodeRabbit

Release Notes

  • New Features

    • Added interactive API documentation interface accessible at /api-docs with searchable endpoint browser
    • Added machine-readable API specification endpoints (/api-docs.json, /api-docs.yaml) for programmatic integration
    • Enabled automatic validation of incoming API requests against documented specifications
  • Documentation

    • API documentation now includes detailed request/response examples and parameter descriptions for all endpoints

Copilot AI review requested due to automatic review settings June 16, 2026 10:47
@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@kydrahul, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 7 minutes and 5 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more credits in the billing tab to continue.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 09e838fb-8bd4-4642-a8a4-dd6a648efc08

📥 Commits

Reviewing files that changed from the base of the PR and between 2ef8815 and ec179d2.

📒 Files selected for processing (7)
  • IMPLEMENTATION_SUMMARY.md
  • backend/src/config/swagger.ts
  • backend/src/middleware/security.ts
  • backend/src/swagger/README.md
  • backend/src/swagger/schemas.yaml
  • coverage.config.js
  • test-validation.js
📝 Walkthrough

Walkthrough

Integrates OpenAPI 3.0.3 documentation into the backend via swagger-jsdoc and swagger-ui-express. Adds a central swagger.ts config, reusable YAML schemas, @openapi JSDoc annotations on all routes, a request validation middleware, Swagger-specific CSP overrides in the security middleware, a type-generation script, and a comprehensive Jest test suite.

Changes

OpenAPI/Swagger Integration

Layer / File(s) Summary
Dependencies and reusable OpenAPI schemas
backend/package.json, backend/src/swagger/schemas.yaml, backend/src/swagger/README.md
Adds swagger-jsdoc, swagger-ui-express, and their @types packages. Defines 12 canonical reusable schemas (ApiError, Account, Transaction, RegisterRequest, LoginRequest, AuthTokenResponse, CreditScoreResponse, FraudDetectionResponse, HealthCheckResult, etc.) in schemas.yaml with typed properties, enums, and validation constraints. README.md documents spec generation commands.
OpenAPI spec config: swaggerSpec, swaggerUiOptions, jsonToYaml
backend/src/config/swagger.ts
Defines the OpenAPI 3.0.3 base object with servers, bearerAuth/apiKeyAuth security schemes, and all component schemas. Configures swagger-jsdoc to merge @openapi JSDoc annotations from route files into swaggerSpec. Exports swaggerUiOptions with custom CSS and UI settings. Exports a recursive jsonToYaml helper.
Request validation middleware
backend/src/middleware/swaggerValidator.ts
New validateRequestAgainstSpec middleware resolves the OpenAPI operation for an incoming request, performs schema-driven validation ($ref resolution, required fields, primitive types, enums, minLength, email format), and returns 422 with structured errors on failure.
Security middleware: /api-docs CSP override
backend/src/middleware/security.ts
Refactors applySecurityMiddleware to disable x-powered-by, conditionally apply trust proxy, add a route-specific Helmet CSP for /api-docs that permits 'unsafe-inline' scripts/styles and Swagger validator image sources, and set referrerPolicy: no-referrer globally.
Express app wiring: Swagger UI and spec endpoints
backend/src/index.ts
Mounts Swagger UI at /api-docs, serves raw spec at /api-docs.json and /api-docs.yaml, registers validateRequestAgainstSpec before /api routes, updates startServer with expanded log output including docs URL, Redis connection check, and process.exit(1) on startup failure.
@openapi JSDoc route annotations
backend/src/routes/accounts.ts, backend/src/routes/auth.ts, backend/src/routes/health.ts
Adds @openapi JSDoc blocks to all existing route handlers documenting path parameters, request bodies, response schemas, and error status codes (400, 403, 409, 422, 429) for accounts, auth, and health endpoints.
Type generation script
backend/scripts/generate-types.ts
Standalone script that imports swaggerSpec at runtime, writes src/types/openapi-spec.json, and optionally invokes openapi-typescript to emit src/types/generated-api.ts, with graceful fallback and exit(1) on fatal errors.
Swagger config tests
backend/src/__tests__/swagger.test.ts
Jest suite asserting swaggerSpec version format, info metadata, servers, tags, security schemes, all component schema names and types, presence and HTTP methods of all documented paths, operation summaries/tags/responses/security/requestBody, and swaggerUiOptions CSS/UI settings.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant SecurityMiddleware
  participant ValidatorMiddleware
  participant ExpressRoute
  participant SwaggerUI

  Client->>SecurityMiddleware: POST /api/accounts (JSON body)
  SecurityMiddleware->>SecurityMiddleware: Apply Helmet CSP + CORS
  SecurityMiddleware->>ValidatorMiddleware: next()
  ValidatorMiddleware->>ValidatorMiddleware: normalisePathForSpec()
  ValidatorMiddleware->>ValidatorMiddleware: resolve $ref schema from swaggerSpec
  ValidatorMiddleware->>ValidatorMiddleware: validateAgainstSchema(req.body)
  alt Validation fails
    ValidatorMiddleware-->>Client: 422 VALIDATION_ERROR + details
  else Validation passes
    ValidatorMiddleware->>ExpressRoute: next()
    ExpressRoute-->>Client: 201 Account created
  end

  Client->>SecurityMiddleware: GET /api-docs
  SecurityMiddleware->>SecurityMiddleware: Apply relaxed CSP (unsafe-inline)
  SecurityMiddleware->>SwaggerUI: next()
  SwaggerUI-->>Client: Swagger UI HTML
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • nexoraorg/chenaikit#128: Introduced the centralized Helmet/CORS/trust-proxy security middleware in backend/src/middleware/security.ts that this PR extends with the /api-docs CSP override.

Poem

🐇 Hippity-hop, I've added the docs,
With schemas and specs under all the locks!
Swagger UI blooms at /api-docs today,
JSDoc annotations light the way.
422 guards the gate with care—
A well-documented API, beyond compare! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat: Add OpenAPI/Swagger documentation' accurately and concisely summarizes the main change—implementing OpenAPI/Swagger documentation for the backend API.
Linked Issues check ✅ Passed All major requirements from #172 are met: OpenAPI spec is complete with schemas, Swagger UI is wired at /api-docs with try-it-out capability, JSDoc annotations added to routes, TypeScript generation script created, validation middleware implemented, and authentication documented.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing OpenAPI/Swagger documentation as specified in #172. No out-of-scope modifications detected.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds first-class OpenAPI/Swagger support to the backend (interactive docs, raw spec endpoints, shared schemas) and introduces request-body validation middleware based on the generated spec.

Changes:

  • Add Swagger/OpenAPI generation (swagger-jsdoc) and Swagger UI (swagger-ui-express) plus JSON/YAML spec endpoints.
  • Add @openapi JSDoc annotations across routes and define canonical component schemas in config/swagger.ts (with reference copies under src/swagger/).
  • Add an Express middleware to validate incoming JSON request bodies against the OpenAPI requestBody schema.

Reviewed changes

Copilot reviewed 12 out of 13 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
backend/src/swagger/schemas.yaml Adds reference component schemas for external tooling.
backend/src/swagger/README.md Documents how OpenAPI files are generated and exported.
backend/src/routes/health.ts Adds OpenAPI docs for health/liveness/readiness endpoints.
backend/src/routes/auth.ts Adds OpenAPI docs for auth endpoints (register/login/refresh).
backend/src/routes/accounts.ts Adds OpenAPI docs for account endpoints, including pagination params.
backend/src/middleware/swaggerValidator.ts Introduces request-body validation middleware using the generated spec.
backend/src/middleware/security.ts Relaxes CSP specifically for Swagger UI routes; reformats imports/strings.
backend/src/index.ts Serves Swagger UI and raw spec endpoints; installs request validation middleware.
backend/src/config/swagger.ts Defines base OpenAPI spec, schemas, and Swagger UI options; adds JSON→YAML conversion.
backend/src/tests/swagger.test.ts Adds unit tests asserting spec structure, paths, tags, and UI options.
backend/scripts/generate-types.ts Adds script to output spec JSON and optionally generate TS types via openapi-typescript.
backend/package.json Adds Swagger dependencies and type packages.
Files not reviewed (1)
  • pnpm-lock.yaml: Generated file

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

Comment on lines +159 to +163
// Only validate requests with JSON bodies
if (!req.body || Object.keys(req.body).length === 0) {
next();
return;
}
Comment on lines +168 to +170
// Try to find matching path in spec
const normalisedPath = normalisePathForSpec(req.path);
const pathSpec = paths[normalisedPath];
Comment on lines +86 to +100
// Relaxed CSP for Swagger UI only — allows inline scripts/styles required by swagger-ui-express
app.use(
"/api-docs",
helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https://validator.swagger.io"],
},
},
crossOriginEmbedderPolicy: false,
}),
);
Comment on lines +220 to +223
it("should include custom CSS to hide the top bar", () => {
expect(swaggerUiOptions.customCss).toContain(".swagger-ui .topbar");
expect(swaggerUiOptions.customCss).toContain("display: none");
});
Comment on lines +5 to +7
components:
schemas:
ApiError:

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
backend/src/routes/health.ts (1)

252-268: ⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

Critical implementation mismatch in the readiness probe.

The /api/health/readiness endpoint documentation promises to check critical services (database, stellar), but the implementation cannot access registered health checks. The healthChecks object (line 22) is never populated; health checks are registered in the dependencies array (via registerHealthCheck(), lines 32-39) instead. As a result, the condition at line 257 (if (healthChecks[name])) will always be false, the loop body never executes, and the endpoint always returns 200 { status: 'ready', services: {} } regardless of actual service status.

The readiness probe is broken and will not serve its intended purpose (checking critical services before directing traffic). The implementation must be corrected to iterate over the dependencies array, filter for critical services, and check their actual health status.

🔧 Proposed fix (example approach)

Option 1: Populate healthChecks during registration:

export function registerHealthCheck(
  name: string,
  check: () => Promise<ServiceHealth>,
  critical: boolean = false
): void {
  dependencies.push({ name, check, critical });
+  healthChecks[name] = check;
  log.info(`Health check registered: ${name}`, { critical });
}

Option 2: Check dependencies array directly in readiness endpoint:

router.get('/health/readiness', async (req: Request, res: Response) => {
-  const criticalServices = ['database', 'stellar'];
+  const criticalDeps = dependencies.filter(d => d.critical);
   const results: Record<string, ServiceHealth> = {};

-  for (const name of criticalServices) {
-    if (healthChecks[name]) {
+  for (const dep of criticalDeps) {
       try {
-        results[name] = await healthChecks[name]();
+        results[dep.name] = await dep.check();
       } catch (error) {
-        return res.status(503).json({ status: 'not ready', error: (error as Error).message });
+        return res.status(503).json({ status: 'not ready', error: (error as Error).message });
       }
     }
   }

   const allUp = Object.values(results).every(s => s.status === 'up');
   res.status(allUp ? 200 : 503).json({ status: allUp ? 'ready' : 'not ready', services: results });
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/routes/health.ts` around lines 252 - 268, The readiness endpoint
is trying to access healthChecks[name], but the healthChecks object is never
populated. Health checks are instead registered in the dependencies array via
registerHealthCheck(). Fix the '/health/readiness' route handler to iterate
directly over the dependencies array, filter for critical services ('database'
and 'stellar'), and call their health check functions from the dependencies
array rather than the non-functional healthChecks object. This will ensure the
endpoint actually checks service status and returns the correct readiness state.
🧹 Nitpick comments (1)
backend/src/index.ts (1)

205-216: 💤 Low value

apiGateway is instantiated but never used.

The ApiGateway instance is created at lines 210-214, but registerGatewayRoutes at line 216 is commented out and apiGateway is never referenced elsewhere. This is dead code that unnecessarily instantiates services.

Either uncomment and complete the gateway registration, or remove the unused instantiation to reduce confusion.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/index.ts` around lines 205 - 216, The apiGateway instance is
created but never used because the registerGatewayRoutes function call is
commented out. Either uncomment the registerGatewayRoutes(apiGateway,
apiKeyService, usageTrackingService) call and ensure it is properly implemented
to register the API gateway routes, or remove the unused instantiation of
ApiGateway along with any service instantiations (apiKeyService,
usageTrackingService, rateLimiter) that are only needed for the gateway if they
are not used elsewhere in the code. Choose the path based on whether the gateway
functionality is intended to be active in this application.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/scripts/generate-types.ts`:
- Around line 34-66: The catch block at line 55 catches all errors (import
failures, generation failures, file write failures) but always logs
"openapi-typescript not installed," masking real generation failures. Refactor
the catch block to distinguish between import errors (where the "not installed"
message is appropriate) and other errors (which should be reported with actual
error details). Check the error type or error message to determine if it's a
module not found error versus a genuine generation or file system failure, and
log accordingly with the actual error information for non-import failures.

In `@backend/src/__tests__/swagger.test.ts`:
- Around line 220-223: The test assertion in the "should include custom CSS to
hide the top bar" test expects `swaggerUiOptions.customCss` to contain "display:
none", but the actual implementation in backend/src/config/swagger.ts applies
positioning rules to hide the topbar instead. Update the test assertion from
expecting "display: none" to expecting the actual CSS property or value that is
used in the swagger configuration to hide the .swagger-ui .topbar element,
ensuring the test reflects what the implementation actually does.

In `@backend/src/index.ts`:
- Around line 67-98: The OpenAPI documentation for the `/api/v1/credit-score`
endpoint declares security requirements (apiKeyAuth and bearerAuth) but the
route handler has no authentication middleware, creating a mismatch between
documented and actual behavior. Apply the apiGateway middleware to the route
handler to enforce authentication, making the implementation match the
documentation. This same fix applies to all other endpoints with security
declarations but missing authentication enforcement.

In `@backend/src/middleware/security.ts`:
- Around line 86-103: The global helmetMiddleware() call at line 102 is being
applied to all routes including `/api-docs`, which overwrites the relaxed CSP
configuration that was specifically set up for the `/api-docs` route. To fix
this, modify the helmetMiddleware() call to skip the `/api-docs` path. This can
be done by adding a conditional skip option to helmetMiddleware() that prevents
it from applying to requests starting with `/api-docs`, ensuring the relaxed CSP
for Swagger UI remains in effect and is not overwritten by the stricter global
helmet configuration.

In `@backend/src/middleware/swaggerValidator.ts`:
- Around line 45-59: The validateAgainstSchema function does not reject
non-object values when the schema requires type "object". Currently it only
validates when the value IS an object, but fails to add an error when the schema
specifies type "object" but the value is an array, null, or a primitive. After
resolving the schema reference, check if resolved.type equals "object" and the
value is not a plain object (i.e., it is null, an array, or a non-object type),
and if so, add an error message to the errors array indicating the type mismatch
before proceeding with further validation logic.
- Around line 159-163: The early return in the swaggerValidator middleware is
incorrectly skipping validation for empty JSON objects `{}` by checking if
`Object.keys(req.body).length === 0`. This prevents validation of required
fields that should be present in the request body. Remove the empty object
length check from the early return condition, keeping only the check for falsy
bodies (truly missing bodies). Instead, add logic after `requestBodySpec` is
resolved to only skip validation when the operation does not define or require a
JSON body in its schema. This allows `validateAgainstSchema(req.body,
jsonSchema)` to properly report missing required fields for empty objects.
- Around line 136-137: The normalisePathForSpec function is being called on
concrete request paths like /api/accounts/123, but it only converts Express
route patterns (with colons) to OpenAPI format (with braces). Since concrete
paths contain actual parameter values, not placeholders, the normalization
returns the path unchanged, preventing any match against the OpenAPI spec keys
like /api/accounts/{id}. Instead of normalizing the concrete path, implement
pattern-based matching that compares the concrete request path against available
OpenAPI template paths to find the matching spec entry. This could be done by
iterating through the paths in the spec and checking if the concrete path
matches each template pattern, handling parameter substitution correctly.

In `@backend/src/swagger/README.md`:
- Line 23: The documentation in backend/src/swagger/README.md at line 23
references a script command `generate:api-types` that does not exist in
backend/package.json. Either identify the correct script name in
backend/package.json that actually generates the API types and update the
documentation to reference that existing script, or add the missing
`generate:api-types` script to backend/package.json with the appropriate command
to generate the openapi-spec.json file.

In `@backend/src/swagger/schemas.yaml`:
- Around line 1-3: The schemas.yaml file is drifting from the canonical runtime
spec defined in backend/src/config/swagger.ts. You need to synchronize the
entire schemas.yaml file (lines 1-217) with the actual schema definitions from
backend/src/config/swagger.ts to eliminate the divergence. Specifically, ensure
that all schema definitions present in the swagger.ts configuration are included
in schemas.yaml, including missing schemas like PaginatedTransactions and any
nested properties like HealthCheckResult.checks.*.details. Update the comment in
schemas.yaml to accurately reflect whether it is truly canonical or a reference
copy, and maintain schema parity between this file and the runtime specification
so that external tools using schemas.yaml remain in sync with what
/api-docs.json actually exposes.

---

Outside diff comments:
In `@backend/src/routes/health.ts`:
- Around line 252-268: The readiness endpoint is trying to access
healthChecks[name], but the healthChecks object is never populated. Health
checks are instead registered in the dependencies array via
registerHealthCheck(). Fix the '/health/readiness' route handler to iterate
directly over the dependencies array, filter for critical services ('database'
and 'stellar'), and call their health check functions from the dependencies
array rather than the non-functional healthChecks object. This will ensure the
endpoint actually checks service status and returns the correct readiness state.

---

Nitpick comments:
In `@backend/src/index.ts`:
- Around line 205-216: The apiGateway instance is created but never used because
the registerGatewayRoutes function call is commented out. Either uncomment the
registerGatewayRoutes(apiGateway, apiKeyService, usageTrackingService) call and
ensure it is properly implemented to register the API gateway routes, or remove
the unused instantiation of ApiGateway along with any service instantiations
(apiKeyService, usageTrackingService, rateLimiter) that are only needed for the
gateway if they are not used elsewhere in the code. Choose the path based on
whether the gateway functionality is intended to be active in this application.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a8021ace-3c6f-4374-b555-e9a26c29ee67

📥 Commits

Reviewing files that changed from the base of the PR and between 52b6e44 and 2ef8815.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (12)
  • backend/package.json
  • backend/scripts/generate-types.ts
  • backend/src/__tests__/swagger.test.ts
  • backend/src/config/swagger.ts
  • backend/src/index.ts
  • backend/src/middleware/security.ts
  • backend/src/middleware/swaggerValidator.ts
  • backend/src/routes/accounts.ts
  • backend/src/routes/auth.ts
  • backend/src/routes/health.ts
  • backend/src/swagger/README.md
  • backend/src/swagger/schemas.yaml

Comment on lines +34 to +66
try {
const openapiTS = await import("openapi-typescript");
const generateFn = openapiTS.default || openapiTS;

if (typeof generateFn === "function") {
const output = await generateFn(swaggerSpec as any);
const tsOutputPath = path.resolve(outputDir, "generated-api.ts");
const content = typeof output === "string" ? output : String(output);
fs.writeFileSync(tsOutputPath, content, "utf-8");
console.log(`✅ TypeScript types written to ${tsOutputPath}`);
} else {
console.log(
"ℹ️ openapi-typescript loaded but generate function not found.",
);
console.log(
" The OpenAPI spec JSON has been saved — you can generate types manually with:",
);
console.log(
` npx openapi-typescript ${specOutputPath} -o ${path.resolve(outputDir, "generated-api.ts")}`,
);
}
} catch {
console.log(
"ℹ️ openapi-typescript not installed. Skipping TypeScript type generation.",
);
console.log(
" Install it with: pnpm add -D openapi-typescript --filter @chenaikit/backend",
);
console.log(" Then re-run this script, or generate types manually with:");
console.log(
` npx openapi-typescript ${specOutputPath} -o ${path.resolve(outputDir, "generated-api.ts")}`,
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Catch block is masking real generation failures as “dependency missing.”

Line 55 catches all errors from import, generation, and file write, but always logs “not installed.” If generation fails for a bad spec or runtime error, this script misreports success conditions and skips actionable failure reporting.

Proposed fix
-  try {
-    const openapiTS = await import("openapi-typescript");
-    const generateFn = openapiTS.default || openapiTS;
-
-    if (typeof generateFn === "function") {
-      const output = await generateFn(swaggerSpec as any);
-      const tsOutputPath = path.resolve(outputDir, "generated-api.ts");
-      const content = typeof output === "string" ? output : String(output);
-      fs.writeFileSync(tsOutputPath, content, "utf-8");
-      console.log(`✅ TypeScript types written to ${tsOutputPath}`);
-    } else {
-      console.log(
-        "ℹ️  openapi-typescript loaded but generate function not found.",
-      );
-      console.log(
-        "   The OpenAPI spec JSON has been saved — you can generate types manually with:",
-      );
-      console.log(
-        `   npx openapi-typescript ${specOutputPath} -o ${path.resolve(outputDir, "generated-api.ts")}`,
-      );
-    }
-  } catch {
+  let generateFn: unknown;
+  try {
+    const openapiTS = await import("openapi-typescript");
+    generateFn = openapiTS.default || openapiTS;
+  } catch {
     console.log(
       "ℹ️  openapi-typescript not installed. Skipping TypeScript type generation.",
     );
@@
-  }
+    return;
+  }
+
+  if (typeof generateFn !== "function") {
+    console.log("ℹ️  openapi-typescript loaded but generate function not found.");
+    return;
+  }
+
+  const output = await generateFn(swaggerSpec as any);
+  const tsOutputPath = path.resolve(outputDir, "generated-api.ts");
+  fs.writeFileSync(tsOutputPath, typeof output === "string" ? output : String(output), "utf-8");
+  console.log(`✅ TypeScript types written to ${tsOutputPath}`);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/scripts/generate-types.ts` around lines 34 - 66, The catch block at
line 55 catches all errors (import failures, generation failures, file write
failures) but always logs "openapi-typescript not installed," masking real
generation failures. Refactor the catch block to distinguish between import
errors (where the "not installed" message is appropriate) and other errors
(which should be reported with actual error details). Check the error type or
error message to determine if it's a module not found error versus a genuine
generation or file system failure, and log accordingly with the actual error
information for non-import failures.

Comment thread backend/src/__tests__/swagger.test.ts
Comment thread backend/src/index.ts
Comment on lines +67 to 98
/**
* @openapi
* /api/v1/credit-score:
* get:
* summary: Get credit score
* description: Returns an AI-generated credit score with contributing factors. Gateway-protected endpoint.
* tags: [Credit Scoring]
* security:
* - apiKeyAuth: []
* - bearerAuth: []
* responses:
* 200:
* description: Credit score computed successfully
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/CreditScoreResponse'
* 401:
* description: Authentication required
* 403:
* description: Insufficient permissions
*/
app.get("/api/v1/credit-score", (_req: Request, res: Response) => {
res.json({
success: true,
data: {
score: Math.floor(Math.random() * 850) + 150,
factors: ['payment_history', 'credit_utilization', 'account_age'],
factors: ["payment_history", "credit_utilization", "account_age"],
timestamp: new Date().toISOString(),
},
});
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

OpenAPI security annotations don't match actual implementation.

The @openapi annotations declare security: [apiKeyAuth, bearerAuth] at lines 74-76 and 107-109, indicating these endpoints require authentication. However, the route handlers at lines 89-98 and 122-132 have no authentication middleware—they return mock data unconditionally.

This mismatch between documented and actual behavior could mislead API consumers into thinking these endpoints are protected when they're not. Either:

  1. Apply the apiGateway middleware to enforce authentication, or
  2. Remove the security declarations from the annotations if these are intentionally public stubs.

Also applies to: 100-132

🧰 Tools
🪛 ast-grep (0.43.0)

[warning] 88-97: Avoid allowing access to unintended directories or files
Context: app.get("/api/v1/credit-score", (_req: Request, res: Response) => {
res.json({
success: true,
data: {
score: Math.floor(Math.random() * 850) + 150,
factors: ["payment_history", "credit_utilization", "account_age"],
timestamp: new Date().toISOString(),
},
});
})
Note: Security best practice.

(path-traversal-typescript)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/index.ts` around lines 67 - 98, The OpenAPI documentation for the
`/api/v1/credit-score` endpoint declares security requirements (apiKeyAuth and
bearerAuth) but the route handler has no authentication middleware, creating a
mismatch between documented and actual behavior. Apply the apiGateway middleware
to the route handler to enforce authentication, making the implementation match
the documentation. This same fix applies to all other endpoints with security
declarations but missing authentication enforcement.

Comment thread backend/src/middleware/security.ts
Comment on lines +45 to +59
function validateAgainstSchema(value: any, schema: any): string[] {
const errors: string[] = [];

if (!schema || !value) return errors;

// Resolve $ref if present
const resolved = schema.$ref ? resolveRef(schema.$ref) : schema;
if (!resolved) return errors;

// Type check
if (
resolved.type === "object" &&
typeof value === "object" &&
!Array.isArray(value)
) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject non-object bodies when the schema requires an object.

For type: "object", arrays/primitives/null currently fall through without adding an error, so malformed bodies can pass validation.

Suggested fix
 function validateAgainstSchema(value: any, schema: any): string[] {
   const errors: string[] = [];
 
-  if (!schema || !value) return errors;
+  if (!schema) return errors;
 
   // Resolve $ref if present
   const resolved = schema.$ref ? resolveRef(schema.$ref) : schema;
   if (!resolved) return errors;
 
   // Type check
-  if (
-    resolved.type === "object" &&
-    typeof value === "object" &&
-    !Array.isArray(value)
-  ) {
+  if (resolved.type === "object") {
+    if (value === null || typeof value !== "object" || Array.isArray(value)) {
+      errors.push("Request body must be an object");
+      return errors;
+    }
+
     // Check required fields
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function validateAgainstSchema(value: any, schema: any): string[] {
const errors: string[] = [];
if (!schema || !value) return errors;
// Resolve $ref if present
const resolved = schema.$ref ? resolveRef(schema.$ref) : schema;
if (!resolved) return errors;
// Type check
if (
resolved.type === "object" &&
typeof value === "object" &&
!Array.isArray(value)
) {
function validateAgainstSchema(value: any, schema: any): string[] {
const errors: string[] = [];
if (!schema) return errors;
// Resolve $ref if present
const resolved = schema.$ref ? resolveRef(schema.$ref) : schema;
if (!resolved) return errors;
// Type check
if (resolved.type === "object") {
if (value === null || typeof value !== "object" || Array.isArray(value)) {
errors.push("Request body must be an object");
return errors;
}
// Check required fields
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/middleware/swaggerValidator.ts` around lines 45 - 59, The
validateAgainstSchema function does not reject non-object values when the schema
requires type "object". Currently it only validates when the value IS an object,
but fails to add an error when the schema specifies type "object" but the value
is an array, null, or a primitive. After resolving the schema reference, check
if resolved.type equals "object" and the value is not a plain object (i.e., it
is null, an array, or a non-object type), and if so, add an error message to the
errors array indicating the type mismatch before proceeding with further
validation logic.

Comment on lines +136 to +137
function normalisePathForSpec(expressPath: string): string {
return expressPath.replace(/:([^/]+)/g, "{$1}");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Show parameterized OpenAPI paths and where the validator is mounted.
rg -nP 'app\.use\(validateRequestAgainstSpec\)|/api/[^"\047`]*\{[^}]+\}' backend/src/index.ts backend/src/routes backend/src/config/swagger.ts

Repository: nexoraorg/chenaikit

Length of output: 317


🏁 Script executed:

cat -n backend/src/middleware/swaggerValidator.ts | sed -n '129,180p'

Repository: nexoraorg/chenaikit

Length of output: 1901


🏁 Script executed:

cat -n backend/src/middleware/swaggerValidator.ts | sed -n '60,127p'

Repository: nexoraorg/chenaikit

Length of output: 2756


🏁 Script executed:

cat -n backend/src/routes/accounts.ts | sed -n '1,20p'

Repository: nexoraorg/chenaikit

Length of output: 880


🏁 Script executed:

# Check the actual spec structure - what does getSpecPaths() return?
rg -A5 'function getSpecPaths|const getSpecPaths' backend/src/middleware/swaggerValidator.ts

Repository: nexoraorg/chenaikit

Length of output: 225


🏁 Script executed:

# Look for how paths are populated in the spec
cat -n backend/src/config/swagger.ts | head -50

Repository: nexoraorg/chenaikit

Length of output: 2261


🏁 Script executed:

# Check what the actual paths object looks like in the spec after building
rg -B2 -A10 'const swaggerSpec|export const swaggerSpec' backend/src/config/swagger.ts

Repository: nexoraorg/chenaikit

Length of output: 501


🏁 Script executed:

# Look at how swagger-jsdoc is called and what files it includes
cat -n backend/src/config/swagger.ts | sed -n '60,120p'

Repository: nexoraorg/chenaikit

Length of output: 2382


🏁 Script executed:

# Find the swagger-jsdoc options configuration
rg -B5 'const options' backend/src/config/swagger.ts | head -30

Repository: nexoraorg/chenaikit

Length of output: 132


🏁 Script executed:

# Let's trace through a concrete example - check one of the documented routes
cat -n backend/src/routes/accounts.ts | sed -n '10,80p'

Repository: nexoraorg/chenaikit

Length of output: 2546


Match concrete request paths against OpenAPI path templates.

req.path is a concrete URL path at runtime (e.g., /api/accounts/123), so calling normalisePathForSpec() on it does not work. The function converts :id to {id}, but /api/accounts/123 contains no colons—it returns unchanged. The OpenAPI spec has /api/accounts/{id} as the key, so paths[normalisedPath] will always be undefined for parameterized routes, causing validation to be skipped.

The documented endpoints with path parameters (GET /api/accounts/{id}, GET /api/accounts/{id}/balance, GET /api/accounts/{id}/transactions) never validate request bodies because their concrete request paths never match the templated spec keys.

Matching must use pattern-based lookup to handle path parameter substitution:

Suggested fix
-function normalisePathForSpec(expressPath: string): string {
-  return expressPath.replace(/:([^/]+)/g, "{$1}");
+function escapeRegex(value: string): string {
+  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
+}
+
+function findPathSpec(paths: Record<string, any>, requestPath: string): any {
+  for (const [specPath, pathSpec] of Object.entries(paths)) {
+    const pattern = `^${escapeRegex(specPath).replace(/\\\{[^/]+\\\}/g, "[^/]+")}/?$`;
+    if (new RegExp(pattern).test(requestPath)) {
+      return pathSpec;
+    }
+  }
+
+  return undefined;
 }
-  // Try to find matching path in spec
-  const normalisedPath = normalisePathForSpec(req.path);
-  const pathSpec = paths[normalisedPath];
+  // Try to find matching path in spec
+  const pathSpec = findPathSpec(paths, req.path);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/middleware/swaggerValidator.ts` around lines 136 - 137, The
normalisePathForSpec function is being called on concrete request paths like
/api/accounts/123, but it only converts Express route patterns (with colons) to
OpenAPI format (with braces). Since concrete paths contain actual parameter
values, not placeholders, the normalization returns the path unchanged,
preventing any match against the OpenAPI spec keys like /api/accounts/{id}.
Instead of normalizing the concrete path, implement pattern-based matching that
compares the concrete request path against available OpenAPI template paths to
find the matching spec entry. This could be done by iterating through the paths
in the spec and checking if the concrete path matches each template pattern,
handling parameter substitution correctly.

Comment on lines +159 to +163
// Only validate requests with JSON bodies
if (!req.body || Object.keys(req.body).length === 0) {
next();
return;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Do not skip {} before checking the operation schema.

An empty JSON object still needs validation for documented bodies with required fields, such as auth requests requiring email and password. This early return lets {} bypass the validator entirely.

Suggested fix
-  // Only validate requests with JSON bodies
-  if (!req.body || Object.keys(req.body).length === 0) {
-    next();
-    return;
-  }
-
   const method = req.method.toLowerCase();
   const paths = getSpecPaths();

Then let validateAgainstSchema(req.body, jsonSchema) report missing required fields; if you want to handle absent bodies separately, do it after requestBodySpec is resolved and only skip when the operation does not define/require a JSON body.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/middleware/swaggerValidator.ts` around lines 159 - 163, The early
return in the swaggerValidator middleware is incorrectly skipping validation for
empty JSON objects `{}` by checking if `Object.keys(req.body).length === 0`.
This prevents validation of required fields that should be present in the
request body. Remove the empty object length check from the early return
condition, keeping only the check for falsy bodies (truly missing bodies).
Instead, add logic after `requestBodySpec` is resolved to only skip validation
when the operation does not define or require a JSON body in its schema. This
allows `validateAgainstSchema(req.body, jsonSchema)` to properly report missing
required fields for empty objects.

Comment thread backend/src/swagger/README.md Outdated
Comment on lines +1 to +3
# Reusable OpenAPI Schemas
# These are the canonical schemas defined in ../config/swagger.ts
# This file serves as a reference copy for external tooling (e.g., client SDK generators).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Canonical schema reference is already drifting from runtime spec.

Line 2 says this file is canonical, but it diverges from backend/src/config/swagger.ts (e.g., missing PaginatedTransactions, and missing HealthCheckResult.checks.*.details). That creates SDK/spec contract drift between tooling using this file and /api-docs.json.

Also applies to: 5-217

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/swagger/schemas.yaml` around lines 1 - 3, The schemas.yaml file
is drifting from the canonical runtime spec defined in
backend/src/config/swagger.ts. You need to synchronize the entire schemas.yaml
file (lines 1-217) with the actual schema definitions from
backend/src/config/swagger.ts to eliminate the divergence. Specifically, ensure
that all schema definitions present in the swagger.ts configuration are included
in schemas.yaml, including missing schemas like PaginatedTransactions and any
nested properties like HealthCheckResult.checks.*.details. Update the comment in
schemas.yaml to accurately reflect whether it is truly canonical or a reference
copy, and maintain schema parity between this file and the runtime specification
so that external tools using schemas.yaml remain in sync with what
/api-docs.json actually exposes.

@kydrahul kydrahul requested a review from Copilot June 16, 2026 11:32

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 15 out of 16 changed files in this pull request and generated 7 comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Generated file

Comment on lines +159 to +163
// Only validate requests with JSON bodies
if (!req.body || Object.keys(req.body).length === 0) {
next();
return;
}
Comment on lines +45 to +48
function validateAgainstSchema(value: any, schema: any): string[] {
const errors: string[] = [];

if (!schema || !value) return errors;
Comment on lines +165 to +170
const method = req.method.toLowerCase();
const paths = getSpecPaths();

// Try to find matching path in spec
const normalisedPath = normalisePathForSpec(req.path);
const pathSpec = paths[normalisedPath];
Comment on lines +297 to +301
const options: swaggerJsdoc.OAS3Options = {
definition: swaggerDefinition,
// Scan route files and index.ts for @openapi annotations
apis: ["./src/routes/*.ts", "./src/index.ts"],
};
Comment on lines +102 to +105
app.use((req, res, next) => {
if (req.path.startsWith("/api-docs")) return next();
return helmetMiddleware()(req, res, next);
});
Comment on lines +338 to +339
export function jsonToYaml(obj: unknown, indent: number = 0): string {
const pad = " ".repeat(indent);
Comment on lines +154 to +158
export function validateRequestAgainstSpec(
req: Request,
res: Response,
next: NextFunction,
): void {
@gelluisaac

Copy link
Copy Markdown
Collaborator

@kydrahul resolve conflicts

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] Add API Documentation with OpenAPI/Swagger

3 participants