Full-Stack Extension (Backend + Frontend)¶
An external API bridge that fetches data through the CORS proxy and displays results in the UI.
spindle.json¶
{
"version": "1.0.0",
"name": "External API Bridge",
"identifier": "api_bridge",
"author": "Dev",
"github": "https://github.com/dev/api-bridge",
"homepage": "https://github.com/dev/api-bridge",
"permissions": ["cors_proxy", "generation"],
"entry_backend": "dist/backend.js",
"entry_frontend": "dist/frontend.js"
}
src/backend.ts¶
declare const spindle: import('lumiverse-spindle-types').SpindleAPI
spindle.onFrontendMessage(async (payload: any, userId) => {
if (payload.type === 'fetch_external') {
try {
const result = await spindle.cors(payload.url, {
method: 'GET',
headers: { 'Accept': 'application/json' },
})
spindle.sendToFrontend({
type: 'external_result',
requestId: payload.requestId,
data: result,
}, userId)
} catch (err: any) {
spindle.sendToFrontend({
type: 'external_error',
requestId: payload.requestId,
error: err.message,
}, userId)
}
}
})
spindle.log.info('API Bridge loaded!')
src/frontend.ts¶
import type { SpindleFrontendContext } from 'lumiverse-spindle-types'
export function setup(ctx: SpindleFrontendContext) {
const removeStyle = ctx.dom.addStyle(`
.api-bridge-panel {
padding: 12px;
background: var(--lumiverse-fill-subtle);
border-radius: var(--lumiverse-radius);
margin: 8px;
}
.api-bridge-panel input {
width: 100%;
padding: 6px 10px;
background: var(--lumiverse-fill);
border: 1px solid var(--lumiverse-border);
border-radius: var(--lumiverse-radius);
color: var(--lumiverse-text);
margin-bottom: 8px;
}
.api-bridge-result {
font-size: 12px;
color: var(--lumiverse-text-muted);
white-space: pre-wrap;
max-height: 200px;
overflow-y: auto;
}
`)
ctx.dom.inject('body', `
<div class="api-bridge-panel">
<input class="api-bridge-input" placeholder="Enter URL to fetch..." />
<div class="api-bridge-result">Results appear here</div>
</div>
`)
const input = ctx.dom.query('.api-bridge-input') as HTMLInputElement
const result = ctx.dom.query('.api-bridge-result')
if (input) {
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && input.value.trim()) {
ctx.sendToBackend({
type: 'fetch_external',
url: input.value.trim(),
requestId: crypto.randomUUID(),
})
if (result) result.textContent = 'Loading...'
}
})
}
const unsub = ctx.onBackendMessage((payload: any) => {
if (result && payload.type === 'external_result') {
result.textContent = JSON.stringify(payload.data, null, 2)
} else if (result && payload.type === 'external_error') {
result.textContent = `Error: ${payload.error}`
}
})
return () => {
unsub()
removeStyle()
ctx.dom.cleanup()
}
}
How It Works¶
- Frontend injects a panel with a URL input and result display area
- When the user presses Enter, the frontend sends a
fetch_externalmessage to the backend with a unique request ID - Backend receives the message, uses the CORS proxy to fetch the URL, and sends the result back
- Frontend displays the JSON response or error message
- Both modules coordinate via the
typefield in messages