Skip to main content
WhatDoc’s API Playground transforms static API documentation into an interactive testing environment. Users can send real HTTP requests, modify headers and payloads, and see live responses — all without leaving your docs.

Overview

The API Playground is automatically generated for backend/API projects when the AI engine detects endpoint definitions in your codebase. Each endpoint gets an embedded playground block that users can expand and interact with.
Playground blocks are only generated for API/backend repositories. Frontend and library projects skip playground generation.

How It Works

The AI engine includes special instructions for API projects:
// From server/services/llm.js (system prompt)

FOR API/BACKEND REPOS ONLYINTERACTIVE PLAYGROUND BLOCKS:
After each endpoint's documentation, append a fenced code block with 
the language tag "json-api-playground" containing a JSON object with 
the endpoint's method, path, default headers, and a sample request body.

Example:

```json-api-playground
{
  "method": "POST",
  "endpoint": "/auth/signup",
  "headers": { "Content-Type": "application/json" },
  "body": { 
    "email": "[email protected]", 
    "password": "securepass123", 
    "username": "newuser" 
  }
}
Rules:
  • ONLY generate for backend/API repos
  • JSON must be valid (no trailing commas)
  • Use realistic sample values from model schemas
  • For GET/DELETE, omit “body” or set to
  • Include auth headers if required

## Playground UI

The playground is a collapsible component that renders from the JSON config:

### Collapsed State

```jsx
<button onClick={() => setExpanded(e => !e)} className="w-full flex items-center justify-between">
  <div className="flex items-center gap-3">
    <span className={`px-2.5 py-0.5 rounded-md text-[11px] font-bold font-mono border ${
      METHOD_COLORS[methodUpper]
    }`}>
      {methodUpper}
    </span>
    <span className="text-sm font-mono text-zinc-300">{endpoint}</span>
  </div>
  <div className="flex items-center gap-2 text-zinc-500 text-xs">
    <span>Try it</span>
    {expanded ? <ChevronUp /> : <ChevronDown />}
  </div>
</button>
Visual Design:
  • Color-coded HTTP method badge (GET=green, POST=blue, DELETE=red)
  • Endpoint path in monospace font
  • “Try it” CTA with chevron icon

Expanded State

When expanded, the playground reveals:
  1. Base URL field (editable)
  2. Headers editor (JSON textarea)
  3. Request body editor (JSON textarea, hidden for GET/DELETE)
  4. Send button with loading state
  5. Response viewer with syntax highlighting

Request Builder

Base URL Configuration

<div>
  <label className="block text-[11px] font-medium text-zinc-500 uppercase tracking-wide mb-1">
    Base URL
  </label>
  <input
    type="text"
    value={baseUrl}
    onChange={(e) => setBaseUrl(e.target.value)}
    className="w-full px-3 py-2 rounded-lg bg-zinc-800 border border-zinc-700 
               text-sm font-mono text-zinc-200"
    placeholder="API base URL"
  />
</div>
Default Value: API_URL from your app’s config (e.g., https://api.whatdoc.xyz)
Users can change the base URL to test against staging, localhost, or custom environments.

Headers Editor

<textarea
  value={headersText}
  onChange={(e) => setHeadersText(e.target.value)}
  rows={3}
  spellCheck={false}
  className="w-full px-3 py-2 rounded-lg bg-zinc-800 border border-zinc-700 
             text-sm font-mono text-zinc-200 resize-y"
/>
Default Headers:
{
  "Content-Type": "application/json"
}
With Authentication:
{
  "Content-Type": "application/json",
  "Authorization": "Bearer YOUR_TOKEN"
}
Invalid JSON in the headers field will display an error message: “Invalid JSON in headers”

Request Body Editor

Only visible for methods that accept a body (POST, PUT, PATCH):
const hasBody = !['GET', 'HEAD', 'DELETE'].includes(methodUpper);

{hasBody && (
  <div>
    <label className="block text-[11px] font-medium text-zinc-500 uppercase tracking-wide mb-1">
      Request Body (JSON)
    </label>
    <textarea
      value={bodyText}
      onChange={(e) => setBodyText(e.target.value)}
      rows={6}
      spellCheck={false}
      className="w-full px-3 py-2 rounded-lg bg-zinc-800 border border-zinc-700 
                 text-sm font-mono text-zinc-200 resize-y"
    />
  </div>
)}
Example Body (from AI-generated config):
{
  "email": "[email protected]",
  "password": "securepass123",
  "username": "newuser"
}

Sending Requests

The Send Request button triggers the fetch call:
const runRequest = useCallback(async () => {
    setLoading(true);
    setError(null);
    setResponse(null);
    setStatusCode(null);

    try {
        let parsedHeaders = {};
        try {
            parsedHeaders = JSON.parse(headersText);
        } catch {
            throw new Error('Invalid JSON in headers');
        }

        const fetchOpts = {
            method: methodUpper,
            headers: { 'Content-Type': 'application/json', ...parsedHeaders },
        };

        if (hasBody && bodyText.trim()) {
            try {
                JSON.parse(bodyText); // validate
            } catch {
                throw new Error('Invalid JSON in request body');
            }
            fetchOpts.body = bodyText;
        }

        const url = `${baseUrl.replace(/\/+$/, '')}${endpoint}`;
        const res = await fetch(url, fetchOpts);
        setStatusCode(res.status);

        const contentType = res.headers.get('content-type') || '';
        if (contentType.includes('application/json')) {
            const json = await res.json();
            setResponse(JSON.stringify(json, null, 2));
        } else {
            const text = await res.text();
            setResponse(text.slice(0, 5000)); // Truncate large responses
        }
    } catch (err) {
        setError(err.message || 'Request failed');
    } finally {
        setLoading(false);
    }
}, [baseUrl, headersText, bodyText, methodUpper, hasBody, endpoint]);
Loading State:
<button onClick={runRequest} disabled={loading}>
    {loading ? (
        <Loader2 className="size-4 animate-spin" />
    ) : (
        <Play className="size-4" />
    )}
    {loading ? 'Sending...' : 'Send Request'}
</button>

Response Viewer

After a successful request, the response appears below:

Status Code Badge

{statusCode && (
  <span className={`px-2 py-0.5 rounded text-[11px] font-mono font-bold ${
    statusCode < 300
      ? 'bg-emerald-500/15 text-emerald-400'
      : statusCode < 500
        ? 'bg-amber-500/15 text-amber-400'
        : 'bg-red-500/15 text-red-400'
  }`}>
    {statusCode}
  </span>
)}
Color Coding:
  • 2xx (Success): Green
  • 3xx-4xx (Client Error): Amber
  • 5xx (Server Error): Red

Response Body

<pre className="px-4 py-3 rounded-lg bg-zinc-950 border border-zinc-800 
                text-sm font-mono text-zinc-300 overflow-x-auto 
                max-h-80 overflow-y-auto whitespace-pre-wrap">
  {response}
</pre>
Features:
  • Syntax-highlighted JSON (if response is JSON)
  • Copy-to-clipboard button
  • Scrollable for long responses
  • Truncated to 5000 characters for non-JSON responses

Copy Response Button

<button onClick={copyResponse} className="flex items-center gap-1 text-xs">
  {copied ? <Check className="size-3" /> : <Copy className="size-3" />}
  {copied ? 'Copied' : 'Copy'}
</button>

Error Handling

Errors are displayed in a red alert box:
{error && (
  <div className="px-4 py-3 rounded-lg bg-red-500/10 border border-red-500/30 
                  text-red-400 text-sm font-mono">
    {error}
  </div>
)}
Common Errors:
  • “Invalid JSON in headers”
  • “Invalid JSON in request body”
  • “Request failed” (network error, CORS, timeout)

CORS & Security

The playground makes requests directly from the user’s browser. If your API doesn’t allow cross-origin requests, the playground will fail with a CORS error.

Enabling CORS for Documentation Testing

Add your docs domain to your API’s CORS whitelist:
// Express.js example
const cors = require('cors');

app.use(cors({
  origin: [
    'https://your-subdomain.whatdoc.xyz',
    'https://docs.your-startup.com'
  ],
  credentials: true
}));
For development, you can temporarily allow all origins: origin: '*'

Playground Configuration Examples

Simple GET Request

{
  "method": "GET",
  "endpoint": "/users",
  "headers": {
    "Authorization": "Bearer YOUR_TOKEN"
  }
}

POST with Complex Body

{
  "method": "POST",
  "endpoint": "/projects",
  "headers": {
    "Content-Type": "application/json",
    "Authorization": "Bearer YOUR_TOKEN"
  },
  "body": {
    "repoName": "acme/api",
    "slug": "acme-api",
    "template": "twilio",
    "isPublic": true
  }
}

DELETE Request (No Body)

{
  "method": "DELETE",
  "endpoint": "/projects/abc123",
  "headers": {
    "Authorization": "Bearer YOUR_TOKEN"
  }
}

Method Color Codes

The playground uses consistent color coding for HTTP methods:
const METHOD_COLORS = {
    GET: 'bg-emerald-500/15 text-emerald-400 border-emerald-500/30',
    POST: 'bg-blue-500/15 text-blue-400 border-blue-500/30',
    PUT: 'bg-amber-500/15 text-amber-400 border-amber-500/30',
    PATCH: 'bg-orange-500/15 text-orange-400 border-orange-500/30',
    DELETE: 'bg-red-500/15 text-red-400 border-red-500/30',
};

Responsive Design

The playground adapts to mobile screens:
  • Desktop: Full-width with side-by-side headers/body
  • Tablet: Stacked layout
  • Mobile: Single-column with larger touch targets

Usage in Generated Docs

When the AI generates documentation for an API endpoint, it includes a playground block:
## POST /auth/signup

Create a new user account.

### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| email | string | Yes | User's email address |
| password | string | Yes | Password (min 8 chars) |
| username | string | Yes | Unique username |

### Example Request

```bash
curl -X POST https://api.whatdoc.xyz/auth/signup \
  -H "Content-Type: application/json" \
  -d '{
    "email": "[email protected]",
    "password": "securepass123",
    "username": "newuser"
  }'
```

### Example Response (201 Created)

```json
{
  "success": true,
  "user": {
    "id": "usr_abc123",
    "email": "[email protected]",
    "username": "newuser"
  },
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
```

### Try It

```json-api-playground
{
  "method": "POST",
  "endpoint": "/auth/signup",
  "headers": { "Content-Type": "application/json" },
  "body": {
    "email": "[email protected]",
    "password": "securepass123",
    "username": "newuser"
  }
}
```

Rendering Logic

The template detects json-api-playground code blocks and renders them as interactive components:
import ReactMarkdown from 'react-markdown';
import ApiPlayground from '../components/ApiPlayground';

<ReactMarkdown
  components={{
    code({ node, inline, className, children, ...props }) {
      const match = /language-(\w+)/.exec(className || '');
      const lang = match?.[1];
      
      // Detect API playground blocks
      if (lang === 'json-api-playground' && !inline) {
        try {
          const config = JSON.parse(String(children));
          return <ApiPlayground config={config} />;
        } catch {
          return <pre>Invalid playground config</pre>;
        }
      }
      
      // Regular code block
      return <SyntaxHighlighter language={lang}>{children}</SyntaxHighlighter>;
    }
  }}
>
  {docs}
</ReactMarkdown>

Performance

  • Bundle size: 8.2 KB (gzipped)
  • First render: < 50ms
  • Request latency: Depends on your API
The playground uses native fetch API — no external HTTP libraries required.

Limitations

  • File uploads: Not supported (use multipart/form-data endpoints require custom handling)
  • Binary responses: Only JSON and text responses are displayed
  • Streaming responses: Not supported (SSE/WebSocket endpoints require custom handling)
  • GraphQL: Not yet supported (coming soon)

Next Steps

AI Generation

Learn how the AI engine detects and documents your API

Custom Domains

Host your interactive docs on your own domain

Build docs developers (and LLMs) love