Skip to content

feat(core): add support for array of global prefixes - #17713

Open
micalevisk wants to merge 2 commits into
nestjs:masterfrom
micalevisk:feat/issue-16095
Open

micalevisk wants to merge 2 commits into
nestjs:masterfrom
micalevisk:feat/issue-16095

Conversation

@micalevisk

@micalevisk micalevisk commented Sep 13, 2026

Copy link
Copy Markdown
Member

PR Checklist

Please check if your PR fulfills the following requirements:

PR Type

What kind of change does this PR introduce?

  • Bugfix
  • Feature
  • Code style update (formatting, local variables)
  • Refactoring (no functional changes, no api changes)
  • Build related changes
  • CI related changes
  • Other... Please describe:

What is the current behavior?

Issue Number: #16095

setGlobalPrefix() only accepts a single string. Before v11, the same routes could be served under several base paths with a regex-style prefix such as (prefixOne|prefixTwo), but path-to-regexp v8 no longer supports that.

What is the new behavior?

setGlobalPrefix() also accepts an array, and every route is registered under each prefix:

app.setGlobalPrefix(['api', 'v1']);
// GET /api/cats and GET /v1/cats
  • ApplicationConfig#getGlobalPrefixes() returns all prefixes.
  • ApplicationConfig#getGlobalPrefix() is now marked @deprecated and will be removed in NestJS v13. Its JSDoc explains that it deliberately keeps returning only the first prefix (as a string) for backward compatibility, and points to getGlobalPrefixes() as the replacement.
  • RoutePathFactory builds one path per prefix. Routes listed in exclude stay unprefixed, as before.
  • RouteInfoPathExtractor builds middleware paths for every prefix, including versioned and wildcard routes.
  • NestApplication#registerRouter() passes all prefixes to RoutesResolver#resolve(), both with and without route conflict detection.

Does this PR introduce a breaking change?

  • Yes
  • No

getGlobalPrefix() still returns a string (the first prefix, or ''), so packages that read it, like @nestjs/swagger, keep working. When several prefixes are set, those packages only see the first one until they switch to getGlobalPrefixes(). That globalPrefixes[0] return is intentional and documented in the method's JSDoc; the method is marked @deprecated so consumers get an editor/compiler hint to migrate to getGlobalPrefixes() before it is removed in v13.

Other information

This supersedes #16102 by @malkovitc, which stalled waiting for a rebase. I cherry-picked their commit onto the current master and kept them as the author. The conflicts in registerRouter() were resolved by keeping the route conflict detection and specificity ordering, with every prefix passed to both resolve() calls.

A deprecation note was left on ApplicationConfig#getGlobalPrefix() stating that returning only the first prefix is intentional to avoid a breaking change, that callers should migrate to getGlobalPrefixes(), and that the legacy method is scheduled for removal in NestJS v13. Core still calls getGlobalPrefix() in registerParserMiddleware(), registerNotFoundHandler() and registerExceptionHandler(), since those adapter hooks accept a single prefix; they are left as-is in this PR.

Impact on @nestjs/swagger

I ran the current @nestjs/swagger master (v12.0.1) against the build from this branch (node_modules/@nestjs/{core,common} symlinked to this branch's packages). Its type-check (tsc -p tsconfig.build.json), unit suite (518 tests) and e2e suite (133 tests) all pass, so this PR does not break it.

However, swagger only ever sees the first prefix, because it reads app.config.getGlobalPrefix() through an untyped any cast (lib/utils/get-global-prefix.ts). With app.setGlobalPrefix(['api', 'v1']):

  • SwaggerModule.createDocument() lists /api/cats but not /v1/cats, even though core serves both.
  • SwaggerModule.setup(..., { useGlobalPrefix: true }) mounts the UI and the JSON/YAML documents under /api/... only; /v1/docs-json returns 404.
  • ignoreGlobalPrefix: true and excluded routes keep working as before.

Since swagger reaches the method via any, the @deprecated tag will not show up in its editor/compiler; the migration hint only reaches them through the JSDoc/changelog. The fix on their side is small: RoutePathFactory#create() already fans out when given getGlobalPrefixes() (verified: it returns ['/api/cats', '/v1/cats']), and swagger already iterates multiple paths per route for versioning, so mostly the helper needs to return string[] (with a fallback to getGlobalPrefix() for older core versions) and setup() needs to loop over the prefixes. I'll open a follow-up issue on nestjs/swagger once this is merged.

Closes nestjs#16095

Ported from nestjs#16102 onto the current master. Conflicts in
`registerRouter()` were resolved by keeping the route conflict
detection and specificity ordering, passing every global prefix to
both `resolve()` calls. The new assertions use vitest's `toEqual`.

(cherry picked from commit d5726a4)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`getGlobalPrefix()` intentionally keeps returning only the first prefix
as a `string` so that existing consumers (e.g. `@nestjs/swagger`) do not
break. Document that, point to `getGlobalPrefixes()`, and mark the
legacy method as deprecated for removal in v13.

Co-Authored-By: Claude Code <noreply@anthropic.com> (claude-fable-5-1)
@alireza-aminzadeh

Copy link
Copy Markdown

Thanks for the thorough writeup on the @nestjs/swagger impact — really useful to have it identified this precisely.

I'd like to take the swagger-side follow-up once this lands, if that's still open. I went through swagger's current source to scope it out ahead of time, and it looks like three spots need updating, not just the helper:

  • lib/utils/get-global-prefix.ts — straightforward: switch to getGlobalPrefixes() with a fallback to getGlobalPrefix() for older @nestjs/core versions.
  • SwaggerModule.setup() — a bit more involved: finalPath, validatedGlobalPrefix, and both JSON/YAML document paths are all derived from a single prefix today, then threaded into serveDocuments()/serveStatic(). Supporting multiple prefixes here means deciding whether to mount the UI/docs under every prefix or just fix the document content while keeping a single mount point.
  • SwaggerScanner.scanApplication() — also takes a single globalPrefix string that feeds into path generation for the document itself, so createDocument() needs the same treatment to actually list routes under every prefix.

Happy to align on the intended UI-mounting behavior before implementing. Let me know if you'd already started on this — otherwise I'll open a PR once #17713 (or its follow-up issue) lands.

@micalevisk

Copy link
Copy Markdown
Member Author

@alireza-aminzadeh go ahead. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants