Blocks
API Request Console Block
Developer UIA copyable developer console for composing API calls, switching environments, validating JSON payloads, inspecting response diagnostics, and saving reusable request templates.
Developer Experience / API Tools
API request console
Copy this into developer dashboards, admin command centers, integration setup flows, internal tools, or customer-facing API explorers that need a runnable request surface.
1200px
<script setup>
import { computed, ref } from 'vue';
import {
DomBadge,
DomButton,
DomCodeInput,
DomDialog,
DomListbox,
DomRangeInput,
DomStatusPill,
DomTagCombobox,
DomTextareaInput,
DomTextInput,
DomToggleButtonGroup,
} from '@getdom/studio/vue';
import RequestTimelineItem from '../components/RequestTimelineItem.vue';
import ResponseStat from '../components/ResponseStat.vue';
const methodOptions = [
{ label: 'GET', value: 'GET' },
{ label: 'POST', value: 'POST' },
{ label: 'PATCH', value: 'PATCH' },
{ label: 'DELETE', value: 'DELETE' },
];
const environments = [
{
label: 'Production',
value: 'prod',
baseUrl: 'https://api.acme.example',
status: 'Protected',
detail: 'Live data, audited proxy',
tone: 'danger',
},
{
label: 'Staging',
value: 'stage',
baseUrl: 'https://staging-api.acme.example',
status: 'Safe',
detail: 'Mirrored schema, test data',
tone: 'success',
},
{
label: 'Local tunnel',
value: 'local',
baseUrl: 'https://dev-tunnel.example',
status: 'Developer',
detail: 'Forwarded to localhost',
tone: 'info',
},
];
const scopeOptions = [
{ label: 'Customers read', value: 'customers:read', description: 'Lookup customer profile and metadata', group: 'Customers' },
{ label: 'Customers write', value: 'customers:write', description: 'Update contact and lifecycle fields', group: 'Customers' },
{ label: 'Billing read', value: 'billing:read', description: 'Inspect invoices, plans, and payment state', group: 'Billing' },
{ label: 'Billing write', value: 'billing:write', description: 'Create invoices and adjust subscriptions', group: 'Billing' },
{ label: 'Events replay', value: 'events:replay', description: 'Replay webhook or event deliveries', group: 'Events' },
{ label: 'Admin command', value: 'admin:command', description: 'Run protected operational commands', group: 'Admin' },
];
const requestExamples = [
{
id: 'create-invoice',
name: 'Create invoice',
method: 'POST',
path: '/v1/invoices',
scopes: ['billing:write', 'customers:read'],
body: `{
"customerId": "cus_1842",
"collectionMethod": "send_invoice",
"daysUntilDue": 14,
"lineItems": [
{ "priceId": "price_growth", "quantity": 3 }
]
}`,
},
{
id: 'lookup-customer',
name: 'Lookup customer',
method: 'GET',
path: '/v1/customers/cus_1842',
scopes: ['customers:read'],
body: '{\n\t"include": ["subscriptions", "riskSignals"]\n}',
},
{
id: 'replay-event',
name: 'Replay event',
method: 'POST',
path: '/v1/events/evt_2091/replay',
scopes: ['events:replay'],
body: `{
"endpointId": "we_prod_platform",
"reason": "Customer requested delivery replay after receiver fix"
}`,
},
];
const method = ref('POST');
const environmentId = ref('stage');
const path = ref('/v1/invoices');
const scopes = ref(['billing:write', 'customers:read']);
const timeoutMs = ref(4500);
const body = ref(requestExamples[0].body);
const templateName = ref('Create draft invoice');
const templateNotes = ref('Use during support-assisted billing setup after confirming customer plan and tax region.');
const isSavingDialogOpen = ref(false);
const isSending = ref(false);
const sendCount = ref(12);
const selectedExampleId = ref('create-invoice');
const lastRunAt = ref('10:44');
const lastStatus = ref(201);
const lastLatency = ref(286);
const lastRequestId = ref('req_9H2kL6');
const selectedEnvironment = computed(() => environments.find((item) => item.value === environmentId.value) || environments[1]);
const selectedExample = computed(() => requestExamples.find((item) => item.id === selectedExampleId.value) || requestExamples[0]);
const fullUrl = computed(() => `${selectedEnvironment.value.baseUrl}${path.value || '/'}`);
const parsedBody = computed(() => {
try {
return { valid: true, value: JSON.parse(body.value || '{}'), error: '' };
} catch (error) {
return { valid: false, value: null, error: error.message };
}
});
const responseTone = computed(() => {
if (!parsedBody.value.valid) return 'danger';
if (lastStatus.value >= 400) return 'danger';
if (lastStatus.value >= 300) return 'warning';
return 'success';
});
const responseBody = computed(() => {
const payload = parsedBody.value.valid ? parsedBody.value.value : { parseError: parsedBody.value.error };
return JSON.stringify({
id: method.value === 'GET' ? 'cus_1842' : 'in_4928',
object: method.value === 'GET' ? 'customer' : 'invoice',
status: parsedBody.value.valid ? 'created' : 'rejected',
request: {
method: method.value,
url: fullUrl.value,
scopes: scopes.value,
timeoutMs: timeoutMs.value,
},
payload,
diagnostics: {
requestId: lastRequestId.value,
latencyMs: lastLatency.value,
proxy: selectedEnvironment.value.label,
audited: true,
},
}, null, 2);
});
const curlSnippet = computed(() => `curl -X ${method.value} "${fullUrl.value}" \\
-H "Authorization: Bearer $API_KEY" \\
-H "Content-Type: application/json" \\
-H "Idempotency-Key: demo-${sendCount.value}" \\
--max-time ${(Number(timeoutMs.value) / 1000).toFixed(1)} \\
-d '${body.value.replace(/\n/g, '')}'`);
const timeline = computed(() => [
{
step: 'Validate request body',
status: parsedBody.value.valid ? 'complete' : 'warning',
time: '12ms',
detail: parsedBody.value.valid ? 'JSON parsed and required content headers attached.' : parsedBody.value.error,
},
{
step: 'Authorize requested scopes',
status: scopes.value.length ? 'complete' : 'warning',
time: '34ms',
detail: scopes.value.length ? `${scopes.value.length} scopes checked against the selected environment.` : 'Add at least one scope before running privileged calls.',
},
{
step: 'Proxy request to API edge',
status: isSending.value ? 'active' : 'complete',
time: `${Math.max(80, Number(timeoutMs.value) - 3920)}ms`,
detail: `Sent through ${selectedEnvironment.value.label.toLowerCase()} with request audit capture enabled.`,
},
{
step: 'Persist run log',
status: 'complete',
time: '18ms',
detail: `Stored as ${lastRequestId.value} for replay, support review, and rate-limit analysis.`,
},
]);
function applyExample(example) {
selectedExampleId.value = example.id;
method.value = example.method;
path.value = example.path;
scopes.value = [...example.scopes];
body.value = example.body;
templateName.value = example.name;
}
function sendRequest() {
isSending.value = true;
window.setTimeout(() => {
sendCount.value += 1;
lastRunAt.value = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
lastLatency.value = parsedBody.value.valid ? 220 + ((sendCount.value * 37) % 180) : 64;
lastStatus.value = parsedBody.value.valid ? (method.value === 'DELETE' ? 202 : method.value === 'GET' ? 200 : 201) : 400;
lastRequestId.value = `req_${String(sendCount.value).padStart(4, '0')}K`;
isSending.value = false;
}, 520);
}
function saveTemplate() {
isSavingDialogOpen.value = false;
}
</script>
<template>
<div class="w-full overflow-hidden rounded-3xl border border-border bg-canvas text-canvas-fg shadow-2xl shadow-black/10">
<header class="border-b border-border skin-card px-4 py-4 sm:px-5">
<div class="flex flex-wrap items-start justify-between gap-4">
<div class="min-w-0">
<p class="text-xs font-semibold uppercase text-muted-fg">Developer workbench</p>
<h3 class="mt-1 text-xl font-semibold">API request console</h3>
<p class="mt-1 max-w-3xl text-sm leading-6 text-muted-fg">
Compose customer-safe API calls, test payloads through a proxy, and save reusable request templates.
</p>
</div>
<div class="flex flex-wrap items-center gap-2">
<DomDialog
v-model="isSavingDialogOpen"
title="Save request template"
description="Name this request and add guidance for teammates before it becomes reusable."
>
<template #trigger>
<DomButton variant="secondary" size="sm">Save template</DomButton>
</template>
<div class="space-y-4">
<DomTextInput v-model="templateName" label="Template name" />
<DomTextareaInput v-model="templateNotes" label="Notes" :rows="4" />
</div>
<template #footer>
<DomButton data-close variant="ghost" size="sm">Cancel</DomButton>
<DomButton size="sm" @click="saveTemplate">Save template</DomButton>
</template>
</DomDialog>
<DomButton size="sm" :loading="isSending" @click="sendRequest">
<svg class="size-4" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M7 5v14l11-7L7 5Z" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round" />
</svg>
Send request
</DomButton>
</div>
</div>
<div class="mt-4 grid gap-3 border-t border-border pt-4 md:grid-cols-[minmax(0,1fr)_minmax(13rem,0.35fr)]">
<div class="grid gap-3 sm:grid-cols-[12rem_minmax(0,1fr)]">
<DomToggleButtonGroup v-model="method" :options="methodOptions" label="Method" size="sm" chrome="none" />
<DomTextInput v-model="path" label="Request path" />
</div>
<div class="rounded-2xl border border-border bg-canvas p-3">
<p class="text-xs font-medium text-muted-fg">Resolved URL</p>
<p class="mt-1 break-all font-mono text-xs leading-5 text-canvas-fg">{{ fullUrl }}</p>
</div>
</div>
</header>
<div class="grid gap-0 xl:grid-cols-[minmax(0,0.95fr)_minmax(22rem,0.55fr)]">
<main class="min-w-0">
<section class="grid gap-4 border-b border-border p-4 sm:p-5 lg:grid-cols-[minmax(0,1fr)_18rem]">
<div class="space-y-4">
<div class="grid gap-4 md:grid-cols-2">
<DomListbox v-model="environmentId" :options="environments" label="Environment">
<template #option="{ option }">
<span class="flex min-w-0 items-start justify-between gap-3">
<span class="min-w-0">
<span class="block text-sm font-semibold">{{ option.label }}</span>
<span class="mt-0.5 block truncate text-xs opacity-75">{{ option.detail }}</span>
</span>
<DomBadge :tone="option.tone" variant="soft" size="sm">{{ option.status }}</DomBadge>
</span>
</template>
</DomListbox>
<div class="grid content-start gap-4">
<DomRangeInput v-model="timeoutMs" label="Timeout budget" :min="1000" :max="10000" :step="500" suffix="ms" />
<DomTagCombobox
v-model="scopes"
:options="scopeOptions"
label="Required scopes"
placeholder="Add scope"
clearable
>
<template #item="{ item }">
<span class="block">
<span class="flex items-center justify-between gap-3">
<span class="font-medium">{{ item.label }}</span>
<span class="text-[11px] text-muted-fg">{{ item.group }}</span>
</span>
<span class="mt-0.5 block text-xs text-muted-fg">{{ item.description }}</span>
</span>
</template>
</DomTagCombobox>
</div>
</div>
<div class="grid gap-4 lg:grid-cols-2">
<DomCodeInput v-model="body" label="Request JSON" lang="json" :rows="14" :editor="false" />
<DomCodeInput :model-value="curlSnippet" label="Generated curl" lang="text" :rows="14" :editor="false" read-only />
</div>
</div>
<aside class="rounded-2xl border border-border skin-card p-3">
<div class="flex items-center justify-between gap-3">
<div>
<p class="text-sm font-semibold">Templates</p>
<p class="mt-1 text-xs text-muted-fg">Start from a safe request shape.</p>
</div>
<DomStatusPill size="sm" tone="success">Synced</DomStatusPill>
</div>
<div class="mt-3 space-y-2">
<button
v-for="example in requestExamples"
:key="example.id"
type="button"
class="w-full rounded-xl border p-3 text-left transition focus-visible:outline-2 focus-visible:outline-ring"
:class="example.id === selectedExample.id ? 'border-primary bg-primary/5' : 'border-border bg-canvas hover:border-primary/40'"
@click="applyExample(example)"
>
<div class="flex items-start justify-between gap-3">
<div class="min-w-0">
<p class="truncate text-sm font-semibold">{{ example.name }}</p>
<p class="mt-1 truncate font-mono text-xs text-muted-fg">{{ example.path }}</p>
</div>
<DomBadge tone="neutral" variant="outline" size="sm">{{ example.method }}</DomBadge>
</div>
<p class="mt-2 text-xs text-muted-fg">{{ example.scopes.length }} scopes required</p>
</button>
</div>
</aside>
</section>
<section class="grid gap-4 p-4 sm:p-5 lg:grid-cols-[minmax(0,0.9fr)_minmax(0,1.1fr)]">
<div class="space-y-4">
<div class="flex flex-wrap items-center justify-between gap-3">
<div>
<p class="text-sm font-semibold">Response diagnostics</p>
<p class="mt-1 text-xs text-muted-fg">Last run {{ lastRunAt }} through {{ selectedEnvironment.label }}.</p>
</div>
<DomStatusPill :tone="responseTone" :pulse="isSending">
{{ isSending ? 'Sending' : `${lastStatus} response` }}
</DomStatusPill>
</div>
<div class="grid gap-3 sm:grid-cols-3">
<ResponseStat label="Status" :value="String(lastStatus)" :detail="parsedBody.valid ? 'Accepted by proxy' : 'Invalid JSON'" :tone="responseTone" />
<ResponseStat label="Latency" :value="`${lastLatency}ms`" detail="Edge round trip" tone="info" />
<ResponseStat label="Request ID" :value="lastRequestId" detail="Audit log key" />
</div>
<ol class="space-y-3 rounded-2xl border border-border bg-canvas p-4">
<RequestTimelineItem
v-for="item in timeline"
:key="item.step"
:step="item.step"
:status="item.status"
:time="item.time"
:detail="item.detail"
/>
</ol>
</div>
<DomCodeInput :model-value="responseBody" label="Response body" lang="json" :rows="20" :editor="false" read-only />
</section>
</main>
</div>
</div>
</template>
Local components
Copy the response helpers
<script setup>
defineProps({
label: {
type: String,
required: true,
},
value: {
type: String,
required: true,
},
detail: {
type: String,
default: '',
},
tone: {
type: String,
default: 'neutral',
},
});
const toneClasses = {
neutral: 'border-border bg-canvas text-canvas-fg',
success: 'border-success/25 bg-success/10 text-success',
warning: 'border-warning/30 bg-warning/15 text-warning-fg',
danger: 'border-destructive/25 bg-destructive/10 text-destructive',
info: 'border-primary/25 bg-primary/10 text-primary',
};
</script>
<template>
<div class="rounded-xl border p-3" :class="toneClasses[tone] || toneClasses.neutral">
<p class="text-[11px] font-medium uppercase opacity-75">{{ label }}</p>
<p class="mt-2 text-lg font-semibold">{{ value }}</p>
<p v-if="detail" class="mt-1 text-xs opacity-75">{{ detail }}</p>
</div>
</template>
<script setup>
defineProps({
step: {
type: String,
required: true,
},
status: {
type: String,
default: 'complete',
},
time: {
type: String,
default: '',
},
detail: {
type: String,
default: '',
},
});
const statusClasses = {
complete: 'bg-success text-success-fg ring-success/20',
active: 'bg-primary text-primary-fg ring-primary/20',
warning: 'bg-warning text-warning-fg ring-warning/30',
waiting: 'bg-secondary text-secondary-fg ring-border',
};
</script>
<template>
<li class="grid grid-cols-[1.5rem_minmax(0,1fr)] gap-3">
<span
class="mt-0.5 flex size-6 items-center justify-center rounded-full text-[11px] font-semibold ring-4"
:class="statusClasses[status] || statusClasses.waiting"
aria-hidden="true"
>
<svg v-if="status === 'complete'" class="size-3.5" viewBox="0 0 24 24" fill="none">
<path d="m5 12.5 4 4L19 7" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" />
</svg>
<svg v-else-if="status === 'active'" class="size-2" viewBox="0 0 8 8" fill="currentColor">
<circle cx="4" cy="4" r="3" />
</svg>
<span v-else>!</span>
</span>
<div class="min-w-0 border-b border-border pb-3">
<div class="flex items-start justify-between gap-3">
<p class="text-sm font-medium text-canvas-fg">{{ step }}</p>
<p v-if="time" class="shrink-0 font-mono text-xs text-muted-fg">{{ time }}</p>
</div>
<p v-if="detail" class="mt-1 text-xs leading-5 text-muted-fg">{{ detail }}</p>
</div>
</li>
</template>
Integration
How to use this block
Use this block when builders need an embedded API playground, webhook tester, customer-support command runner, or internal platform console. The pattern keeps request setup, auth context, payload editing, send feedback, response metrics, and reusable templates in one copyable workflow.
- Replace the sample environment, endpoint, scope, and timeline arrays with workspace-scoped API metadata from your backend.
- Send requests through a server-owned proxy so credentials, audit logging, rate limits, and allowlisted hosts stay protected.
- Validate JSON request bodies before dispatch and return structured field errors when the server rejects a payload.
- Persist saved templates with method, path, environment, scopes, headers, body, timeout, owner, and visibility.
- Store every run as an immutable attempt record so support, security, and developer-success teams can inspect what happened.
Data
Recommended request template shape
{
id: 'reqtpl_create_invoice',
workspaceId: 'wrk_platform_2048',
name: 'Create draft invoice',
visibility: 'team',
method: 'POST',
environmentId: 'env_stage',
path: '/v1/invoices',
timeoutMs: 4500,
scopes: ['billing:write', 'customers:read'],
headers: [
{ name: 'Idempotency-Key', value: 'invoice-{{customer.id}}-{{date}}', secret: false },
{ name: 'Authorization', value: 'Bearer {{api_key}}', secret: true }
],
body: {
customerId: 'cus_1842',
collectionMethod: 'send_invoice',
lineItems: [{ priceId: 'price_growth', quantity: 3 }]
},
lastRun: {
status: 201,
latencyMs: 286,
requestId: 'req_9H2kL6',
ranAt: '2026-06-12T10:44:00Z'
}
}Customization
Implementation notes
Security boundary
Never run customer-entered requests directly from the browser with privileged credentials. Proxy and audit calls on your server, then redact secrets before returning logs.
Schema ownership
Drive available endpoints, required scopes, headers, examples, and validation messages from your API schema or command catalog.
Future updates
Useful follow-ups include OpenAPI import, curl export, request diffing, signed replay links, secret variable pickers, and promotion of response metrics into a shared block component.