feat-api-docs-swagger-scalar
✨ New Features
API documentation (Scalar + Swagger UI)
Goal: Expose the neo-backend HTTP API as browsable, interactive documentation so developers (and AI agents) can discover routes, read schemas, paste a Bearer token and fire real requests.
Solution: Code-first OpenAPI via @nestjs/swagger (CLI plugin enabled, so DTO
schemas and route descriptions are derived from the code/comments). The generated
document is served through two UIs over the same spec: Scalar on /docs and Swagger UI
on /swagger, with the raw spec on /openapi.json and /openapi.yaml. Bearer auth is declared in the spec
and both UIs allow authorizing and calling endpoints.
Features:
- Scalar API reference on
/docs - Swagger UI on
/swagger(with persisted authorization) - Raw OpenAPI spec on
/openapi.json(JSON) and/openapi.yaml(YAML, e.g. for Retool/Postman import) - Bearer token authentication preselected in both UIs
Implementation Details:
@nestjs/swaggerCLI plugin (introspectComments) enabled innest-cli.json.- Doc routes excluded from the auth middleware so the documentation is publicly reachable.
- Worked around a
@nestjs/swagger+bson@6issue: the plugin emitsrequire("bson/bson")forObjectId-typed properties, a subpath bson@6 no longer exports. A module-load redirect (bson/bson->bson) inmain.tsfixes the crash; the affected DTO properties also get explicit@ApiProperty({ type: String })for a clean schema.
Files Modified:
package.json/package-lock.json- Add@nestjs/swaggerand@scalar/nestjs-api-referencenest-cli.json- Enable the@nestjs/swaggerCLI plugin (introspectComments)src/main.ts- Build the OpenAPI document, serve Scalar (/docs) and Swagger UI (/swagger), add thebson/bsonmodule-load redirectsrc/app.module.ts- Exclude doc routes from the auth middlewaresrc/domains/company/attributes.dto.ts-@ApiProperty({ type: String })on ObjectId fieldssrc/domains/document/document.type.ts-@ApiProperty({ type: String })on ObjectId field.env.example- Document thePROOF_WATCHER_ENABLEDenv var
⚡ Improvements
API documentation accuracy & usability
Goal: Make the generated OpenAPI faithfully reflect the real API so the docs are trustworthy and the "Try it out" calls actually work.
Solution: Fix the spots where the @nestjs/swagger plugin silently diverged from the
code (file-suffix scanning, TS-only required/optional detection, untyped params) and
declare the auth requirement globally so both UIs send the token.
Features:
- Endpoints with inline DTOs now show their query params / request bodies (were "No parameters").
- Optional fields are no longer wrongly marked as required.
- Stats endpoints (
/stats/*) now document theirlevel,startDate,endDate,companyIdparams. - Both UIs send the Bearer token on every request (real calls succeed instead of 401).
Implementation Details:
- Extend the plugin
dtoFileNameSuffixto.controller.ts/.type.tsso DTOs declared inline are annotated. - Add a global Bearer security requirement (
addSecurityRequirements) — the scheme was declared but never applied, so the UIs sent noAuthorizationheader. - Mark
@IsOptional()fields optional in TS (?) or via@ApiPropertyOptional()(the plugin derivesrequiredfrom TS optionality and ignores class-validator). - Type previously untyped
@Query('x')/@Param('x')params (the plugin skips implicitany). - Normalize the shared
ObjectIdschema to a 24-hex string instead of an empty object. - Drop the hardcoded
addServer('/api/v2')that double-prefixed requests (/api/v2/api/v2/...). - Add
npm run openapi:generate(preview mode, DB-less) to dump the spec for inspection / static export.
Files Modified:
nest-cli.json- ExtenddtoFileNameSuffixto scan controllers/typessrc/main.ts- Global Bearer requirement,normalizeObjectIdSchema, removeaddServersrc/generate-openapi.ts- Offline OpenAPI generator (preview mode)package.json-openapi:generatescript.gitignore- Ignoreopenapi.generated.jsonsrc/domains/document/document.controller.ts- OptionalDocumentsSearchDtofields, typed badge paramsrc/domains/stat/stat.controller.ts- Typed@Query/@Param(level: StatLevel, dates/companyId)src/domains/company/attributes.dto.ts- Optional fields (?/@ApiPropertyOptional),lov: string[]README.md- Document the docs setup, annotation gotchas and the SCIM exclusion TODO
Documented response models & error envelope
Goal: Turn the spec into a real contract — every operation exposes its error shape,
and the flagship document endpoints expose typed 2xx responses (were an empty 200).
Solution: A shared ErrorResponse DTO and an @ApiStandardErrors() decorator that
mirror GlobalExceptionFilter, applied across the documented controllers, plus decorated
response classes for the documents domain.
Features:
- Every documented route now declares
400/401/403/404/500with the realErrorResponseenvelope (statusCode,message,error,correlationId?,timestamp,path). POST /documents,GET /documents,/documents/filtersand/documents/from-idexpose typed response schemas instead of an empty body.GET /documentsquery paramsfolder,attributesandautoAttributesare now typed in the spec.- Object-array DTO fields (
targetAutoAttributes) now expose{code, value}item schemas instead of being wrongly inferred asstring[]— strict clients (Scalar MCP) accept the correct payload.
Files Modified:
src/shared/openapi/error-response.dto.ts- SharedErrorResponseschema (new)src/shared/openapi/api-standard-errors.decorator.ts-@ApiStandardErrors()decorator (new)src/domains/document/document.responses.ts- Decorated response models for the documents domain (new)src/domains/document/document.controller.ts-@ApiOkResponse/@ApiCreatedResponse+ typedfolder/attributes/autoAttributesquery paramssrc/domains/{company,stat,export,client-error}/...controller.tsandsrc/domains/platform/*/*.controller.ts-@ApiStandardErrors()on the documented surface
Absolute OpenAPI server for off-origin consumers (Scalar MCP)
Goal: Let off-origin tools (Scalar's hosted MCP) actually call the API. They fail with
"Server base URL is not configured" because the spec exposes no servers and the global
prefix (/api/v2) is not part of the paths — a browser infers the base from the page
origin, but an off-origin MCP cannot.
Solution: When API_PUBLIC_URL is set, expose it as the absolute servers entry
(the bare origin — runtime paths already carry the /api/v2 global prefix, so appending it
would double it). Applied both at runtime and in the offline generator (which now mirrors the
global prefix). Left empty locally; the doc UIs keep working unchanged.
Files Modified:
src/core/config/app.config.ts- Add optionalpublicUrl(API_PUBLIC_URL)src/main.ts-addServer(<API_PUBLIC_URL>/api/v2)when configuredsrc/generate-openapi.ts- Same server entry in the offline spec.env.example- DocumentAPI_PUBLIC_URL
🔒 Security
Fix dependency vulnerabilities
Goal: Clear all high- and medium-severity Snyk findings so the security scan passes in CI.
Solution: Upgrade direct dependencies and pin patched transitive versions via overrides.
Files Modified:
package.json/package-lock.json- Upgradeaxios(SSRF / prototype pollution / sensitive data),json-2-csv(CSV injection),joi(uncaught exception),qs(null pointer dereference); overridemulter@2.2.0(uncontrolled recursion),ws@8.21.0(resource amplification),brace-expansion@5.0.6underminimatch@9/@10(ReDoS),form-data@4.0.6(CRLF injection) andjs-yaml@4.2.0(algorithmic complexity)