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.

Scalar console mount path The specification is bundled at build time, served as a static asset, and read by the Scalar component which renders the reference and the request console. openapi/ tree multi-file, $refs bundle one resolved document static asset /openapi/api.yaml Scalar reference + console in the browser skip the bundle step and the browser silently renders empty schemas

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
Configuration flags and what they prevent Bundling prevents empty schemas, sandbox-first server order prevents accidental production writes, and disabling auth persistence prevents credential leakage. each setting prevents one specific, silent failure bundle the spec prevents empty schema tables fails silently otherwise sandbox first in servers prevents writes to real data one click from a docs page persistAuth: false prevents a token in localStorage survives the tab

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.

Console health checklist Operations listed, schema tables populated, default server is the sandbox, and a send returns a real status — each can fail while the page still looks correct. check in this order — an earlier failure explains every later one 1. operations list spec loaded at all empty → wrong URL 2. schema tables refs resolved empty → not bundled 3. default server sandbox selected prod → reorder servers 4. send returns a real status code nothing → CORS or CSP