Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions MIGRATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,24 @@ Breaking changes and upgrade notes for downstream projects.

---

## Redoc replaces Scalar for /api/docs (2026-04-13)

The `/api/docs` UI is now served by [redoc-express](https://www.npmjs.com/package/redoc-express) instead of `@scalar/express-api-reference`. Redoc renders the same OpenAPI spec (`/api/spec.json`) with a cleaner three-panel layout better suited to a consumer-facing API reference (no try-it-out panel — the API is API-key-gated and meant for programmatic use).

### What changed

- `package.json` — `@scalar/express-api-reference` removed, `redoc-express` added
- `lib/services/express.js` — `initSwagger` mounts `redoc({ title, specUrl: '/api/spec.json', redocOptions: { hideDownloadButton, hideSchemaTitles, expandResponses } })` instead of the Scalar middleware. Spec assembly, guides loader, YAML merge, and `/api/spec.json` handler are unchanged.
- `lib/helpers/guides.js` — comments updated (Scalar → Redoc); behavior unchanged.
- `modules/core/tests/core.integration.tests.js` — `describe('Redoc API reference', …)` rename; assertions (HTML content-type, valid OpenAPI spec) unchanged.

### Action for downstream

1. Run `/update-stack` to pull the change — no project-side YAML, config, or CSP tweaks required.
2. Visual check: hit `/api/docs` and confirm the new Redoc UI renders the merged spec (guides sidebar + endpoint reference).

---

## Rate limiter keys by userId + trust proxy (2026-04-08)

Rate-limit middleware now keys authenticated requests by `user._id` (with `req.ip` fallback) instead of always using IP. Production config enables `trust.proxy: 1` so `req.ip` reflects the real client IP behind a single reverse proxy (Traefik, Nginx).
Expand Down
8 changes: 4 additions & 4 deletions lib/helpers/guides.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
/**
* Markdown guide loader for the Scalar API reference.
* Markdown guide loader for the Redoc API reference.
*
* Per-module markdown guides live under `modules/{name}/doc/guides/*.md`
* and are discovered by the same globbing mechanism as OpenAPI YAML files
* (see `config/assets.js` → `allGuides`).
*
* Guides are merged into the OpenAPI spec via `info.description`, which
* Scalar renders as a top-level "Introduction" section in the sidebar and
* Redoc renders as a top-level "Introduction" section in the sidebar and
* splits on markdown H1/H2 headings.
*/
import fs from 'fs';
Expand All @@ -31,7 +31,7 @@ const titleFromPath = (filePath) => {

/**
* Strip the first H1 heading from a markdown body (if present).
* The loader injects its own H1 based on the file name so Scalar's sidebar
* The loader injects its own H1 based on the file name so Redoc's sidebar
* stays consistent even when guides omit a title or use a different one.
* @param {string} markdown - Raw markdown content.
* @returns {string} Markdown without the leading H1.
Expand Down Expand Up @@ -70,7 +70,7 @@ const loadGuides = (filePaths) => {

/**
* Merge loaded guides into an OpenAPI spec's `info.description`.
* Each guide becomes a top-level H1 section, which Scalar renders as a
* Each guide becomes a top-level H1 section, which Redoc renders as a
* sidebar entry alongside the API reference.
*
* The original spec is mutated (and returned) to match the merge style used
Expand Down
24 changes: 16 additions & 8 deletions lib/services/express.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import cors from 'cors';
import morgan from 'morgan';
import fs from 'fs';
import YAML from 'js-yaml';
import { apiReference } from '@scalar/express-api-reference';
import redoc from 'redoc-express';

import config from '../../config/index.js';
import guidesHelper from '../helpers/guides.js';
Expand All @@ -26,7 +26,7 @@ import AnalyticsService from './analytics.js';
import analyticsMiddleware from '../middlewares/analytics.js';

/**
* Initialize API documentation (Scalar UI + JSON spec endpoint)
* Initialize API documentation (Redoc UI + JSON spec endpoint)
* @param {object} app - express application instance
* @returns {void}
*/
Expand Down Expand Up @@ -75,7 +75,7 @@ const initSwagger = (app) => {
};
spec.servers = [{ url: config.domain || 'http://localhost:3000' }];

// Merge per-module markdown guides into info.description so Scalar
// Merge per-module markdown guides into info.description so Redoc
// renders them in its sidebar alongside the OpenAPI reference.
const guides = guidesHelper.loadGuides(config.files.guides || []);
guidesHelper.mergeGuidesIntoSpec(spec, guides);
Expand All @@ -96,12 +96,20 @@ const initSwagger = (app) => {
// Serve the merged spec as JSON
app.get('/api/spec.json', serveSpec);

// Mount Scalar API reference UI
app.use(
// Mount Redoc API reference UI — consumes the spec via URL (not inline).
// Equivalents for the previous Scalar `hideModels` behavior: hide the
// download button and schema titles, and expand common success responses
// so the reference feels compact and consumer-focused.
app.get(
'/api/docs',
apiReference({
spec: { content: spec },
hideModels: true,
redoc({
title: config.app.title,
specUrl: '/api/spec.json',
redocOptions: {
hideDownloadButton: true,
hideSchemaTitles: true,
expandResponses: '200,201',
},
}),
);
}
Expand Down
6 changes: 3 additions & 3 deletions modules/core/tests/core.integration.tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ describe('Core integration tests:', () => {
});
});

describe('Scalar API reference', () => {
describe('Redoc API reference', () => {
it('should expose /api/spec.json with a valid OpenAPI info block', async () => {
const res = await request(app).get('/api/spec.json').expect(200);
expect(res.body).toBeDefined();
Expand All @@ -313,9 +313,9 @@ describe('Core integration tests:', () => {
expect(res.body.servers[0].url.length).toBeGreaterThan(0);
});

it('should serve the Scalar API reference page on /api/docs', async () => {
it('should serve the Redoc API reference page on /api/docs', async () => {
const res = await request(app).get('/api/docs').expect(200);
// Scalar returns HTML referencing the spec URL
// Redoc returns HTML referencing the spec URL
expect(res.headers['content-type']).toMatch(/html/);
});
});
Expand Down
3 changes: 2 additions & 1 deletion modules/core/tests/core.unit.tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -513,7 +513,8 @@ describe('Core unit tests:', () => {
const mockApp = { get: mockGet, use: mockUse };
expressService.initSwagger(mockApp);
expect(mockGet).toHaveBeenCalledWith('/api/spec.json', expect.any(Function));
expect(mockUse).toHaveBeenCalledWith('/api/docs', expect.any(Function));
// Redoc middleware is a plain request handler mounted via app.get
expect(mockGet).toHaveBeenCalledWith('/api/docs', expect.any(Function));
});

it('should serve merged spec as JSON from /api/spec.json handler', () => {
Expand Down
Loading
Loading