Frontend Integration

Integrate Next.js, React, and other frontend frameworks with Magic Runtime's controller API. Type-safe client, error handling, and real-time patterns.

API Overview

The Magic Runtime exposes a REST API for executing controllers. All endpoints accept and return JSON.

Method Endpoint Description
POST /api/execute/{controller} Execute a controller
GET /api/controllers List deployed controllers
GET /api/controllers/{name} Get controller metadata + contract
GET /api/health Health check
GET /api/readyz Readiness check

Request / Response Example

bash
# Execute a controller
curl -X POST https://your-magic-instance/api/execute/InvoiceSync \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"customer_id": "cust_123", "invoice_data": {"amount": 9900}}'

# Response
{
  "result": { "sync_id": "inv_456", "status": "completed" },
  "request_id": "req_abc123",
  "duration_ms": 142,
  "controller": "InvoiceSync",
  "version": "1.0.0"
}

Tracing

All responses include request_id for tracing. Pass it in support tickets or log queries.

TypeScript Client

A type-safe client wrapper that handles authentication, error parsing, and response typing.

lib/magic-client.ts
// lib/magic-client.ts

interface MagicResponse<T> {
  result: T;
  request_id: string;
  duration_ms: number;
  controller: string;
  version: string;
}

interface MagicError {
  error: string;
  code: string;       // E-code (e.g., E1001)
  request_id: string;
  detail?: string;
}

// Two auth modes: API Key (service-to-service) or Bearer token (user sessions)
type AuthMode =
  | { mode: "api-key"; key: string }
  | { mode: "bearer"; token: string };

class MagicClient {
  private baseUrl: string;
  private auth: AuthMode;

  constructor(baseUrl: string, auth: AuthMode) {
    this.baseUrl = baseUrl;
    this.auth = auth;
  }

  private authHeader(): Record<string, string> {
    return this.auth.mode === "api-key"
      ? { "X-API-Key": this.auth.key }
      : { "Authorization": `Bearer ${this.auth.token}` };
  }

  async execute<TInput, TOutput>(
    controller: string,
    input: TInput
  ): Promise<MagicResponse<TOutput>> {
    const res = await fetch(
      `${this.baseUrl}/api/execute/${controller}`,
      {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          ...this.authHeader(),
        },
        body: JSON.stringify(input),
      }
    );

    if (!res.ok) {
      const err: MagicError = await res.json();
      throw new MagicExecutionError(err);
    }

    return res.json();
  }

  async listControllers(): Promise<string[]> {
    const res = await fetch(`${this.baseUrl}/api/controllers`, {
      headers: this.authHeader(),
    });
    return res.json();
  }
}

class MagicExecutionError extends Error {
  code: string;
  requestId: string;
  detail?: string;

  constructor(err: MagicError) {
    super(err.error);
    this.name = "MagicExecutionError";
    this.code = err.code;
    this.requestId = err.request_id;
    this.detail = err.detail;
  }
}

// API Key mode (service-to-service, scripts, Server Actions)
export const magic = new MagicClient(
  process.env.NEXT_PUBLIC_MAGIC_URL!,
  { mode: "api-key", key: process.env.MAGIC_API_KEY! }
);

// Bearer mode (user sessions, JWT forwarding)
// export const magic = new MagicClient(
//   process.env.NEXT_PUBLIC_MAGIC_URL!,
//   { mode: "bearer", token: userJwt }
// );

Next.js Integration

Use Server Actions to call Magic Runtime from Next.js components while keeping your API token server-side.

Server Action

app/actions/sync-invoice.ts
// app/actions/sync-invoice.ts
"use server";

import { magic } from "@/lib/magic-client";

interface SyncInput {
  customer_id: string;
  invoice_data: { amount: number; currency: string };
}

interface SyncResult {
  sync_id: string;
  status: string;
}

export async function syncInvoice(input: SyncInput) {
  const response = await magic.execute<SyncInput, SyncResult>(
    "InvoiceSync",
    input
  );
  return response.result;
}

API Route

app/api/controllers/[name]/route.ts
// app/api/controllers/[name]/route.ts
import { magic } from "@/lib/magic-client";
import { NextRequest, NextResponse } from "next/server";

export async function POST(
  req: NextRequest,
  { params }: { params: { name: string } }
) {
  const input = await req.json();

  try {
    const result = await magic.execute(params.name, input);
    return NextResponse.json(result);
  } catch (err) {
    if (err instanceof MagicExecutionError) {
      return NextResponse.json(
        { error: err.message, code: err.code },
        { status: 422 }
      );
    }
    return NextResponse.json(
      { error: "Internal error" },
      { status: 500 }
    );
  }
}

Security

Server Actions keep your MAGIC_API_KEY on the server. Never expose it to the browser.

React Hooks

A custom hook for executing controllers from client components, with loading state, error handling, and request tracing.

useMagic Hook

hooks/use-magic.ts
// hooks/use-magic.ts
import { useState, useCallback } from "react";

interface UseMagicOptions {
  onError?: (error: Error) => void;
}

export function useMagic<TInput, TOutput>(
  controller: string,
  options?: UseMagicOptions
) {
  const [data, setData] = useState<TOutput | null>(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<Error | null>(null);
  const [requestId, setRequestId] = useState<string | null>(null);

  const execute = useCallback(
    async (input: TInput) => {
      setLoading(true);
      setError(null);

      try {
        const res = await fetch(`/api/controllers/${controller}`, {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify(input),
        });

        if (!res.ok) {
          const err = await res.json();
          throw new Error(err.error || "Execution failed");
        }

        const response = await res.json();
        setData(response.result);
        setRequestId(response.request_id);
        return response.result;
      } catch (err) {
        const error = err instanceof Error ? err : new Error(String(err));
        setError(error);
        options?.onError?.(error);
        throw error;
      } finally {
        setLoading(false);
      }
    },
    [controller, options]
  );

  return { execute, data, loading, error, requestId };
}

Usage in a Component

components/InvoiceForm.tsx
// components/InvoiceForm.tsx
"use client";

import { useMagic } from "@/hooks/use-magic";

export function InvoiceForm() {
  const { execute, loading, error, requestId } = useMagic<
    { customer_id: string; invoice_data: object },
    { sync_id: string; status: string }
  >("InvoiceSync");

  async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault();
    const formData = new FormData(e.currentTarget);

    await execute({
      customer_id: formData.get("customerId") as string,
      invoice_data: { amount: Number(formData.get("amount")), currency: "usd" },
    });
  }

  return (
    <form onSubmit={handleSubmit}>
      <input name="customerId" placeholder="Customer ID" required />
      <input name="amount" type="number" placeholder="Amount (cents)" required />
      <button type="submit" disabled={loading}>
        {loading ? "Syncing..." : "Sync Invoice"}
      </button>
      {error && <p className="error">{error.message}</p>}
      {requestId && <p className="meta">Request: {requestId}</p>}
    </form>
  );
}

Error Handling

Magic Runtime uses structured E-codes to categorize errors. Map these to appropriate frontend behavior.

Code Category Meaning Frontend Action
E1xxx CONTRACT Input/output schema violation Show validation errors to user
E2xxx CAPABILITY Controller lacks permission Internal error — do not expose
E3xxx EXECUTION Controller runtime error Show generic error + request_id
E4xxx DEPLOYMENT Controller not found Check controller name spelling
E5xxx AUTH Authentication failure Redirect to login

Error Classification Helper

lib/magic-errors.ts
// lib/magic-errors.ts

export function classifyError(code: string): "validation" | "auth" | "internal" {
  if (code.startsWith("E1")) return "validation";
  if (code.startsWith("E5")) return "auth";
  return "internal";
}

export function userMessage(code: string, detail?: string): string {
  switch (classifyError(code)) {
    case "validation":
      return detail || "Please check your input and try again.";
    case "auth":
      return "Your session has expired. Please log in again.";
    case "internal":
      return "Something went wrong. Please try again or contact support.";
  }
}

Debugging

Always log the request_id — it maps directly to Magic Runtime traces for debugging.

Authentication

Configure your environment with the Magic Runtime URL and API token.

.env.local (Next.js)
# .env.local (Next.js)
NEXT_PUBLIC_MAGIC_URL=https://magic.your-company.com

# Mode A: API Key (service-to-service, Server Actions, SSR)
# This is the ADMIN_API_KEY value from your Magic Runtime .env file
MAGIC_API_KEY=your-admin-api-key-from-magic-runtime

# Mode B: Bearer / JWT (user sessions — token comes from your auth system)
# No env var needed; pass the JWT from the user's session

Warning

MAGIC_API_KEY is a server-side secret. Use the NEXT_PUBLIC_ prefix only for the base URL. The key must never reach the browser.

Pattern When to Use
X-API-Key (API Key) Server Actions, API Routes, SSR, scripts — use MAGIC_API_KEY env var
Authorization: Bearer (JWT) User sessions, SSO integration — token from your auth system
Session Proxy Client components calling through your API route (API key stays server-side)

Real-Time Updates

For real-time data, use polling or server-sent events until WebSocket support arrives in a future release.

Polling Hook

hooks/use-magic-poll.ts
// hooks/use-magic-poll.ts
import { useEffect } from "react";
import { useMagic } from "./use-magic";

export function useMagicPoll<T>(
  controller: string,
  input: T,
  intervalMs: number = 5000
) {
  const { execute, data, loading } = useMagic(controller);

  useEffect(() => {
    execute(input);
    const id = setInterval(() => execute(input), intervalMs);
    return () => clearInterval(id);
  }, [controller, intervalMs]);

  return { data, loading };
}

Roadmap

WebSocket support is planned for v2.3 (Admin Console). Until then, use polling or server-sent events through your own API layer.

Testing

Test your Magic Runtime integration with mocked fetch calls using Vitest or Jest.

__tests__/magic-client.test.ts
// __tests__/magic-client.test.ts
import { describe, it, expect, vi } from "vitest";
import { magic } from "@/lib/magic-client";

// Mock fetch for tests
global.fetch = vi.fn();

describe("MagicClient", () => {
  it("executes a controller", async () => {
    (fetch as any).mockResolvedValueOnce({
      ok: true,
      json: async () => ({
        result: { sync_id: "inv_1", status: "completed" },
        request_id: "req_test",
        duration_ms: 50,
        controller: "InvoiceSync",
        version: "1.0.0",
      }),
    });

    const res = await magic.execute("InvoiceSync", {
      customer_id: "cust_1",
      invoice_data: { amount: 100 },
    });

    expect(res.result.status).toBe("completed");
    expect(fetch).toHaveBeenCalledWith(
      expect.stringContaining("/api/execute/InvoiceSync"),
      expect.objectContaining({ method: "POST" })
    );
  });

  it("throws MagicExecutionError on failure", async () => {
    (fetch as any).mockResolvedValueOnce({
      ok: false,
      json: async () => ({
        error: "Schema validation failed",
        code: "E1001",
        request_id: "req_err",
      }),
    });

    await expect(
      magic.execute("InvoiceSync", { bad: "input" })
    ).rejects.toThrow("Schema validation failed");
  });
});

Next Steps