Skip to content
This repository was archived by the owner on Aug 17, 2026. It is now read-only.

Commit 889bb16

Browse files
pascal-klesseRalph Loopclaude
authored
feat(openapi): legacy /api-docs-json alias for nuxt-base-starter compat (#60)
* test(openapi): add red tests for legacy /api-docs-json alias Failing story tests covering: - /api-docs-json passes the JWT middleware (public path) - /api-docs-json passes the tenant guard (exempt path) - GET /api-docs-json returns the same document as /api/openapi.json - GET /api-docs-json sets Deprecation + Link successor-version headers Drives the upcoming bootstrap shim that keeps older nuxt-base-starter installations (whose openapi-ts.config.ts fallback hardcodes /api-docs-json) working until lenneTech/nuxt-base-starter#13 ships. * feat(openapi): legacy /api-docs-json alias for nuxt-base-starter compat Mounts /api-docs-json as a deprecated alias of /api/openapi.json so older nuxt-base-starter installations (whose openapi-ts.config.ts fallback hardcodes the legacy path) keep generating types without manual config edits. The handler returns the same OpenAPI document plus RFC 8594 `Deprecation` and RFC 8288 `Link: rel="successor-version"` headers pointing clients at the canonical URL. Path is added to the JWT middleware and tenant-guard exempt sets so the Express-mounted shim mirrors `/api/openapi.json`'s public-access posture. Tracked upstream at lenneTech/nuxt-base-starter#13 — remove this shim once the fix has propagated. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(architecture): note legacy /api-docs-json alias + deprecation timeline Adds a short paragraph under "Zod → OpenAPI bridge" pointing at the canonical /api/openapi.json URL and explaining the deprecated /api-docs-json alias plus the upstream issue (lenneTech/nuxt-base-starter#13) gating its removal. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Ralph Loop <ralph@local> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent ac49216 commit 889bb16

5 files changed

Lines changed: 111 additions & 2 deletions

File tree

docs/architecture.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,17 @@ The slim-module reference and the user-profile module are the two
209209
canonical examples — copy their decorator pattern when you scaffold
210210
a new module.
211211

212+
`/api/openapi.json` is the canonical OpenAPI doc URL.
213+
`/api-docs-json` is mounted as a **deprecated alias** for legacy
214+
`nuxt-base-starter` installations whose `openapi-ts.config.ts`
215+
fallback still hardcodes the path used by older
216+
`nest-server-starter` releases. The alias returns the same document
217+
plus `Deprecation` (RFC 8594) and `Link: rel="successor-version"`
218+
(RFC 8288) headers pointing clients at the canonical URL. It will
219+
be removed once the upstream fix
220+
([lenneTech/nuxt-base-starter#13](https://github.com/lenneTech/nuxt-base-starter/issues/13))
221+
has propagated to all consumer workspaces.
222+
212223
## Multi-tenancy
213224

214225
Two-layer isolation:

src/core/app/bootstrap.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,22 @@ export async function bootstrap(options: BootstrapOptions = {}): Promise<INestAp
119119
app.use("/api/openapi.json", (_req: any, res: any) => {
120120
res.json(openApiDocument);
121121
});
122+
// Legacy alias for `nuxt-base-starter` installations whose
123+
// `openapi-ts.config.ts` fallback still hardcodes `/api-docs-json`
124+
// (the path used by older versions of `nest-server-starter`).
125+
// Tracked upstream at
126+
// https://github.com/lenneTech/nuxt-base-starter/issues/13;
127+
// remove once that fix has propagated to all consumer workspaces.
128+
// The `Deprecation` (RFC 8594) + `Link` (RFC 8288, `successor-version`)
129+
// headers tell well-behaved clients to migrate to /api/openapi.json.
130+
//
131+
// @deprecated use /api/openapi.json
132+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
133+
app.use("/api-docs-json", (_req: any, res: any) => {
134+
res.setHeader("Deprecation", "Sat, 31 Oct 2026 23:59:59 GMT");
135+
res.setHeader("Link", '</api/openapi.json>; rel="successor-version"');
136+
res.json(openApiDocument);
137+
});
122138
if (cfg.env !== "production") {
123139
// eslint-disable-next-line @typescript-eslint/no-explicit-any
124140
app.use("/api/openapi", (req: any, res: any, next: any) => {

src/core/auth/jwt-middleware.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,14 @@
1414
* `/api/openapi.json` serve the OpenAPI spec the SDK generators read.
1515
* Both are dev-friendly (they expose error codes / route shapes only,
1616
* never user data) and intentionally outside the auth wall.
17+
*
18+
* `/api-docs-json` is the deprecated legacy alias for the OpenAPI
19+
* doc, kept exempt for the same reason as `/api/openapi.json` until
20+
* the upstream `nuxt-base-starter` fix
21+
* (lenneTech/nuxt-base-starter#13) has propagated.
1722
*/
1823
const PUBLIC_PREFIXES = ["/health/", "/api/auth/", "/docs/", "/dev/", "/errors/", "/api/openapi"];
19-
const PUBLIC_EXACT = new Set(["/", "/errors", "/api/openapi"]);
24+
const PUBLIC_EXACT = new Set(["/", "/errors", "/api/openapi", "/api-docs-json"]);
2025

2126
export function isPathProtected(path: string): boolean {
2227
if (!path) throw new Error("isPathProtected: path is required");

src/core/multi-tenancy/tenant-guard.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@
88
* The actual NestJS Guard wraps this classifier in a future slice.
99
*/
1010

11-
const EXEMPT_EXACT = new Set(["/", "/errors", "/tenants"]);
11+
// `/api-docs-json` is the deprecated legacy alias for
12+
// `/api/openapi.json` — exempt from the tenant header because SDK
13+
// generators that hit the legacy URL don't carry a tenant context
14+
// (mirrors the canonical doc's exemption). Removed once
15+
// lenneTech/nuxt-base-starter#13 has propagated.
16+
const EXEMPT_EXACT = new Set(["/", "/errors", "/tenants", "/api-docs-json"]);
1217
// `/me/*` endpoints operate on the authenticated user (req.user.id),
1318
// not on a specific tenant. `/tenants` is the self-service tenant CRUD
1419
// surface — a signed-up user creates their first tenant here, so the
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import type { INestApplication } from "@nestjs/common";
2+
import request from "supertest";
3+
import { afterAll, beforeAll, describe, expect, it } from "vitest";
4+
5+
import { isPathProtected } from "../../src/core/auth/jwt-middleware.js";
6+
import { isTenantExempt } from "../../src/core/multi-tenancy/tenant-guard.js";
7+
import { bootstrap } from "../../src/core/app/bootstrap.js";
8+
9+
const SILENT_LOGGER = { log() {}, warn() {}, error() {}, debug() {}, verbose() {} };
10+
11+
/**
12+
* Story · Legacy `/api-docs-json` alias.
13+
*
14+
* Older `nuxt-base-starter` workspaces hard-code
15+
* `http://127.0.0.1:3000/api-docs-json` in their `openapi-ts.config.ts`
16+
* fallback. Since this server canonicalised the OpenAPI doc at
17+
* `/api/openapi.json`, those workspaces 401 on `pnpm run generate-types`
18+
* until they upgrade. We mount `/api-docs-json` as a deprecated alias
19+
* that returns the exact same document plus `Deprecation` /
20+
* `Link: rel="successor-version"` headers, buying time until the
21+
* upstream fix (lenneTech/nuxt-base-starter#13) propagates.
22+
*
23+
* Tracked at https://github.com/lenneTech/nuxt-base-starter/issues/13.
24+
*/
25+
describe("Story · Legacy /api-docs-json alias", () => {
26+
describe("auth + tenant exemptions", () => {
27+
it("treats /api-docs-json as public (no JWT required)", () => {
28+
expect(isPathProtected("/api-docs-json")).toBe(false);
29+
});
30+
31+
it("treats /api-docs-json as tenant-exempt", () => {
32+
expect(isTenantExempt("/api-docs-json")).toBe(true);
33+
});
34+
});
35+
36+
describe("HTTP behaviour", () => {
37+
let app: INestApplication;
38+
39+
beforeAll(async () => {
40+
app = await bootstrap({ listen: false, logger: SILENT_LOGGER });
41+
});
42+
43+
afterAll(async () => {
44+
await app?.close();
45+
});
46+
47+
it("returns the same OpenAPI document as /api/openapi.json", async () => {
48+
const canonical = await request(app.getHttpServer()).get("/api/openapi.json");
49+
const legacy = await request(app.getHttpServer()).get("/api-docs-json");
50+
expect(canonical.status).toBe(200);
51+
expect(legacy.status).toBe(200);
52+
// Compare the structural body — both endpoints serve the same
53+
// SwaggerModule document instance, so deep equality must hold.
54+
expect(legacy.body).toEqual(canonical.body);
55+
expect(legacy.body?.openapi).toBeDefined();
56+
expect(legacy.body?.paths).toBeDefined();
57+
});
58+
59+
it("sends a Deprecation header pointing clients at the canonical URL", async () => {
60+
const res = await request(app.getHttpServer()).get("/api-docs-json");
61+
expect(res.status).toBe(200);
62+
// RFC 8594 — Deprecation header signals to clients (and human
63+
// log-readers) that the endpoint is on its way out.
64+
expect(res.headers.deprecation).toBeDefined();
65+
// RFC 8288 Link header with rel="successor-version" points at
66+
// /api/openapi.json. SDK generators that respect the hint can
67+
// self-heal without a config change.
68+
expect(res.headers.link).toContain("</api/openapi.json>");
69+
expect(res.headers.link).toContain('rel="successor-version"');
70+
});
71+
});
72+
});

0 commit comments

Comments
 (0)