Adding a Try-It Console with Scalar
This guide is part of Interactive API Consoles & Try-It Panels within Developer Portal Frameworks & UI Setup. It walks through mounting a Scalar API reference with a working request console — the concrete install behind the design decisions the parent guide covers. The situation that leads here is a reference page that reads well and leaves the reader with no way to try anything, so the first real request happens in a terminal an hour later, if at all.
Problem & Context
Scalar reads an OpenAPI document in the browser and renders both the reference and a request client from it. That single input is also the single point of failure, and it fails quietly.
A specification split across files — paths/invoices.yaml, schemas/Invoice.yaml, and so on — is normal and good practice for authoring, as Defining JSON Schema Components describes. But a browser fetching the root document has no way to resolve a relative $ref to a sibling file that was never published. The console renders, the operations list correctly, and every schema table is empty. Nothing errors, because from the renderer’s point of view an unresolvable reference is simply an unknown schema.
The second failure is subtler and more damaging. If servers lists production first, the console defaults to it, and a reader clicking Send on DELETE /invoices/{id} from a documentation page has just deleted a real invoice. Both problems are configuration, and both are fixed before the console is ever mounted.
Step-by-Step Solution
1. Bundle the specification
Resolve everything into one document as a build step, and publish that file rather than the source tree:
npx @redocly/cli@2 bundle openapi/api.yaml -o public/openapi/api.bundled.yaml
Confirm nothing is left unresolved:
grep -c '\$ref:.*\.yaml' public/openapi/api.bundled.yaml
0
A non-zero count means external references survived the bundle, and the console will render those schemas empty.
2. Order the servers array sandbox-first
The console selects the first entry by default, so the order in the specification is a safety control rather than a stylistic choice:
servers:
- url: https://sandbox.api.example.com/v1
description: Sandbox — disposable data, safe to call from these docs
- url: https://api.example.com/v1
description: Production — real data
3. Mount the reference
For a plain HTML page, a script tag and a spec URL are the whole integration:
<!doctype html>
<html lang="en">
<head><meta charset="utf-8"><title>Acme API reference</title></head>
<body>
<script
id="api-reference"
data-url="/openapi/api.bundled.yaml"></script>
<script>
var configuration = {
theme: 'default',
persistAuth: false, // never write credentials to localStorage
hideDownloadButton: false, // let readers take the spec away
defaultHttpClient: { targetKey: 'shell', clientKey: 'curl' },
};
document.getElementById('api-reference')
.dataset.configuration = JSON.stringify(configuration);
</script>
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
</body>
</html>
For a React portal, the component form takes the same configuration object:
import { ApiReferenceReact } from '@scalar/api-reference-react';
import '@scalar/api-reference-react/style.css';
export default function Reference() {
return (
<ApiReferenceReact
configuration={{
url: '/openapi/api.bundled.yaml',
persistAuth: false,
// Route requests through a same-origin proxy so CORS never applies.
proxyUrl: '/api-proxy',
}}
/>
);
}
4. Disable credential persistence
persistAuth: false is the single most important line above. With persistence on, a token the reader pastes is written to localStorage, where it outlives the tab, is readable by any script on the origin, and gets captured in screenshots attached to support tickets.
5. Verify the console in a real browser
A console that renders is not a console that works. Drive it headlessly and assert both the response and the absence of stored credentials:
node -e "
const { chromium } = require('playwright');
(async () => {
const b = await chromium.launch();
const p = await b.newPage();
const errors = [];
p.on('console', m => { if (m.type() === 'error') errors.push(m.text()); });
await p.goto('https://docs.example.com/reference/');
await p.getByRole('button', { name: /send/i }).first().click();
await p.waitForSelector('[data-testid=\"response-status\"]', { timeout: 15000 });
const status = (await p.textContent('[data-testid=\"response-status\"]')).trim();
const stored = await p.evaluate(() =>
Object.keys(localStorage).filter(k => /auth|token|key/i.test(k)));
console.log('status:', status, '| stored credentials:', stored.length);
if (errors.length) { console.error(errors); process.exit(1); }
await b.close();
})();
"
status: 200 OK | stored credentials: 0
Complete Working Example
A build script that bundles, validates the safety properties, and publishes — so a specification edit cannot silently break the console:
#!/usr/bin/env bash
# build-reference.sh — bundle the spec and assert the console's safety invariants
set -euo pipefail
SPEC_SRC="openapi/api.yaml"
SPEC_OUT="public/openapi/api.bundled.yaml"
mkdir -p "$(dirname "${SPEC_OUT}")"
# 1. Valid before anything else looks at it.
npx @redocly/cli@2 lint "${SPEC_SRC}"
# 2. Resolve every $ref into one document.
npx @redocly/cli@2 bundle "${SPEC_SRC}" -o "${SPEC_OUT}"
# 3. No external references may survive the bundle.
if grep -q '\$ref:[[:space:]]*[^#]' "${SPEC_OUT}"; then
echo "ERROR: unresolved external \$ref in ${SPEC_OUT}" >&2
exit 1
fi
# 4. The first server must be a sandbox, or the console defaults to production.
first_server=$(yq -r '.servers[0].url' "${SPEC_OUT}")
case "${first_server}" in
*sandbox*|*staging*) echo "OK default server: ${first_server}" ;;
*) echo "ERROR: first server is not a sandbox: ${first_server}" >&2; exit 1 ;;
esac
# 5. At least one operation must carry an example, or the console shows placeholders.
examples=$(yq -r '[.paths[][]?.responses[]?.content[]?.examples // empty] | length' "${SPEC_OUT}")
[ "${examples}" -gt 0 ] || { echo "ERROR: no response examples in the spec" >&2; exit 1; }
echo "OK bundled ${SPEC_SRC} → ${SPEC_OUT} (${examples} example set(s))"
Expected output:
OK default server: https://sandbox.api.example.com/v1
OK bundled openapi/api.yaml → public/openapi/api.bundled.yaml (14 example set(s))
Those four assertions are worth more than the mount code they protect. Each corresponds to a failure that renders a perfectly functional-looking console useless or dangerous, and each is invisible in a code review of the specification diff.
Gotchas & Edge Cases
The console loads before the spec is published. In a static build the reference page and the bundled spec are separate artifacts, and a deploy that uploads HTML before assets produces a brief window where the page mounts against a missing file. The console renders an empty shell with no error. Upload the specification before the HTML that references it, as Caching and CDN headers for docs sites describes.
A stale cached specification outlives a deploy. The bundled spec has a stable URL and changing content, so a long max-age pins the console to an old contract. Readers then see operations that no longer exist and get confusing errors when they call them. Serve it with a short lifetime and an ETag.
CDN-hosted Scalar pins nothing. The cdn.jsdelivr.net/npm/@scalar/api-reference URL above resolves to the latest release, so the console can change under you without any commit in your repository. Pin an exact version in production, and treat a version bump as a change to be previewed like any other.
Content Security Policy blocks the request before CORS does. If the docs site sets a connect-src directive, the console can only call hosts on that list — including your own proxy path. A blocked request produces a TypeError: Failed to fetch with no network entry at all, which looks identical to a CORS failure but is fixed in a completely different place.
Examples do more work than any styling option. The console populates its request body from the specification’s examples, so an operation with a realistic example gives the reader a request they can send unmodified, while one without gives them a skeleton of "string" and 0 that they must fill in before anything useful happens. That gap is the difference between a reader making a successful call in ten seconds and abandoning the page. Investing in examples improves the rendered reference, the console, and any mock server generated from the same document at once — which makes it the highest-leverage specification work available. Example Payload Management covers keeping them accurate as the API changes.
Decide what happens on the very first visit. A reader arriving with no credential should still be able to press Send and see something. If the sandbox requires authentication and the console has no token, every operation returns 401 and the console reads as broken rather than as gated. Either supply a demo credential server-side through the proxy, or make the sandbox’s read operations open, so the first interaction always succeeds. Reserve the authentication prompt for the moment a reader tries something that genuinely needs it.
FAQ
Why do schemas render as empty objects in the console?
The specification was loaded unbundled, so external $ref pointers never resolved in the browser. Bundle to a single document at build time, publish that file, and assert that no external references survive — the check in the build script above does exactly this.
Can I use Scalar without a build step?
Yes. A script tag plus a data-url attribute pointing at a spec URL is enough for a plain HTML page, which is why Scalar suits embedding a reference into an existing site rather than adopting a whole framework. You still want the bundling step, but it can run wherever the specification lives.
How do I stop the console from remembering an API key?
Set persistAuth: false. The default in some integrations writes the credential to localStorage, where it survives the tab and is readable by any script on the origin. Assert it stays empty in the headless check, as shown above.
A quick way to tell whether the console is genuinely healthy rather than merely rendering: check these four things in order, because each one failing produces a page that still looks fine.
Related
- Interactive API Consoles & Try-It Panels — the parent guide and the request-path decision.
- Proxying CORS for API try-it consoles — the
proxyUrlthis configuration points at. - Injecting API keys safely into docs consoles — where the credential comes from.
- Scalar & Modern Docs Integration — the wider Scalar setup and theming.