# @superblocksteam/sdk-api

TypeScript SDK for defining Superblocks APIs as simple, type-safe functions.

## Overview

This SDK enables writing Superblocks APIs as TypeScript functions with:

- **Zod schemas** for input/output validation
- **Typed integration clients** for Postgres, Slack, OpenAI, Anthropic, Stripe, GitHub, Notion, and more
- **Full type inference** for inputs, outputs, and integrations
- **Upfront integration declarations** for type-safe access and pre-authentication
- **Runtime schema validation** with detailed error information for debugging

## Architecture & Execution

This section explains how the SDK works internally - what an API is, what `api()` produces, and how APIs are executed.

### What is an API?

An **API** in this SDK is a self-contained, typed function definition that:

1. **Declares its contract** - Input/output schemas define what data it accepts and returns
2. **Declares its dependencies** - Integration references specify which external services it needs
3. **Implements logic** - The `run` function contains the business logic
4. **Is portable** - Can be executed by any compatible runtime (local, server, orchestrator)

An API is **not** just a function - it's a complete definition that includes metadata, schemas, and dependencies, enabling the runtime to:

- Validate inputs before execution
- Pre-authenticate integrations
- Validate outputs after execution
- Provide detailed error information

### What `api()` Produces

The `api()` function compiles your configuration into a `CompiledApi` object:

```typescript
interface CompiledApi<TInput, TOutput> {
  /** Name for identification in logs and debugging */
  readonly name: string;

  /** Zod schema for validating inputs */
  readonly inputSchema: z.ZodType<TInput>;

  /** Zod schema for validating outputs */
  readonly outputSchema: z.ZodType<TOutput>;

  /** The implementation function */
  readonly run: (ctx: ApiContext, input: TInput) => Promise<TOutput>;

  /** Integration declarations for upfront authentication */
  readonly integrations: ReadonlyArray<IntegrationDeclaration>;
}
```

#### Identifying an API

You can inspect a `CompiledApi` to understand what it is:

```typescript
import { api, z, postgres } from "@superblocksteam/sdk-api";

const myApi = api({
  name: "GetUsers",
  integrations: { db: postgres("prod-postgres-id") },
  input: z.object({ limit: z.number() }),
  output: z.object({ users: z.array(z.object({ id: z.string() })) }),
  async run(ctx, { limit }) {
    /* ... */
  },
});

// Inspect the compiled API
console.log(myApi.name); // 'GetUsers'
console.log(myApi.integrations); // [{ key: 'db', pluginId: 'postgres', integrationId: 'prod-postgres-id' }]

// Schemas can be used for validation or type extraction
type Input = z.infer<typeof myApi.inputSchema>; // { limit: number }
type Output = z.infer<typeof myApi.outputSchema>; // { users: { id: string }[] }
```

### How APIs are Executed

The `executeApi()` function runs a compiled API with the required inputs:

```typescript
import { executeApi } from '@superblocksteam/sdk-api';

const response = await executeApi(myApi, {
  // Raw input data (will be validated against inputSchema)
  input: { limit: 10 },

  // Integration configurations (provided by the runtime)
  integrations: [
    { id: 'prod-postgres-id', name: 'Production DB', pluginId: 'postgres', configuration: {...} }
  ],

  // Unique ID for tracing/debugging
  executionId: 'exec_abc123',

  // Environment variables accessible via ctx.env
  env: { NODE_ENV: 'production' },

  // Callback to execute integration operations (see below)
  executeQuery: async (integrationId, request, bindings) => {
    return await orchestrator.execute(integrationId, request, bindings);
  },
});

// Response is a discriminated union
if (response.success) {
  console.log(response.output);  // Typed as { users: { id: string }[] }
} else {
  console.error(response.error.code, response.error.message);
}
```

#### Execution Flow

1. **Input Validation** - Raw input is validated against `api.inputSchema`
2. **Context Creation** - `ApiContext` is created with integration clients and logger
3. **API Execution** - The `run` function is called with context and validated input
4. **Output Validation** - Return value is validated against `api.outputSchema`
5. **Response** - Returns `{ success: true, output }` or `{ success: false, error }`

### The Callback Architecture

The SDK doesn't directly communicate with databases or external APIs. Instead, it uses **callbacks** that the runtime provides. This enables the SDK to run in any environment.

#### `executeQuery` Callback

When an integration client (like `ctx.integrations.db`) makes a request, it calls the `executeQuery` callback:

```typescript
// Inside PostgresClient.query():
const result = await executeQuery(
  "prod-postgres-id", // Integration ID
  {
    // Plugin-specific request (matches protobuf schema)
    body: "SELECT * FROM users WHERE id = $1",
  },
  { userId: "123" }, // Optional bindings for language plugins
);
```

The runtime (orchestrator) receives this callback and:

1. Looks up the integration configuration
2. Authenticates with the external service
3. Executes the actual operation (SQL query, API call, etc.)
4. Returns the result to the SDK

This architecture means:

- **SDK is environment-agnostic** - Same code runs locally, in tests, or in production
- **Authentication is centralized** - The runtime handles all credentials
- **Operations are auditable** - The runtime can log/trace all operations

### Execution Request Interface

The complete request interface for `executeApi()`:

```typescript
interface ExecuteApiRequest {
  /** Raw input data to be validated */
  input: unknown;

  /** Available integration configurations */
  integrations: IntegrationConfig[];

  /** Unique execution ID for tracing */
  executionId: string;

  /** Environment variables available via ctx.env */
  env: Record<string, string>;

  /**
   * Callback to execute integration operations.
   * Called by integration clients (postgres, slack, etc.) to perform actual operations.
   */
  executeQuery: (
    integrationId: string,
    request: Record<string, unknown>, // Plugin-specific request matching protobuf schema
    bindings?: Record<string, unknown>, // For language plugins (JavaScript)
  ) => Promise<unknown>;
}
```

### Execution Response

`executeApi()` returns a discriminated union:

```typescript
type ExecuteApiResponse<TOutput> =
  | { success: true; output: TOutput }
  | {
      success: false;
      error: { code: string; message: string; details?: unknown };
    };
```

### Integration Configuration

Each integration in the `integrations` array has this structure:

```typescript
interface IntegrationConfig {
  /** Unique integration ID (referenced in api() config) */
  id: string;

  /** Display name */
  name: string;

  /** Plugin type (e.g., 'postgres', 'slack', 'openai_v2') */
  pluginId: string;

  /** Plugin-specific configuration (credentials, endpoints, etc.) */
  configuration: Record<string, unknown>;
}
```

The SDK matches integration declarations in your API (`integrations: { db: postgres('id') }`) with configurations in the request by ID.

## Installation

```bash
pnpm add @superblocksteam/sdk-api
```

## Quick Start

```typescript
import { api, z, postgres } from "@superblocksteam/sdk-api";

// Integration IDs from the integrations panel - store in constants
const PROD_POSTGRES = "a1b2c3d4-5678-90ab-cdef-ghijklmnopqr";

const UserSchema = z.object({
  id: z.string(),
  name: z.string(),
  email: z.string().email(),
});

export default api({
  name: "GetUserById",

  // Declare integrations upfront for type safety
  integrations: {
    db: postgres(PROD_POSTGRES),
  },

  input: z.object({
    userId: z.string().uuid(),
  }),

  output: z.object({
    user: UserSchema,
  }),

  async run(ctx, { userId }) {
    // ctx.integrations.db is fully typed as PostgresClient
    const users = await ctx.integrations.db.query(
      "SELECT * FROM users WHERE id = $1",
      UserSchema, // Schema is REQUIRED
      [userId],
    );

    if (users.length === 0) {
      throw new Error("User not found");
    }

    return { user: users[0] };
  },
});
```

### Export style (default vs named exports)

**Default export** — one API per file. **Named exports** — several `api({ ... })` values in one module; import them by name into `server/apis/index.ts`. `useApi("X")` matches the **key** on the registry object; align `api({ name: "X" })` with that when practical.

```typescript
// server/apis/users/pair.ts
import { api, z } from "@superblocksteam/sdk-api";

export const ListProfiles = api({
  name: "ListProfiles",
  input: z.object({ orgId: z.string() }),
  output: z.object({ profiles: z.array(z.object({ id: z.string() })) }),
  async run() {
    return { profiles: [] };
  },
});

export const UpdateProfile = api({
  name: "UpdateProfile",
  input: z.object({ userId: z.string() }),
  output: z.object({ ok: z.literal(true) }),
  async run() {
    return { ok: true as const };
  },
});
```

```typescript
// server/apis/index.ts
import { ListProfiles, UpdateProfile } from "./users/pair.js";

const apis = { ListProfiles, UpdateProfile } as const;
export default apis;
export type ApiRegistry = typeof apis;
```

## API Reference

### `api(config)`

Defines a TypeScript-based API with input/output validation. Returns a complete result when the `run` function completes.

#### Required Fields

| Field          | Type                              | Description                                                                                                                                            |
| -------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `name`         | `string`                          | **Required.** Unique identifier for the API                                                                                                            |
| `description`  | `string`                          | Optional. Plain-language summary of what this API does and which integrations it uses. Auto-generated by the AI agent when creating or editing an API. |
| `input`        | `z.ZodType`                       | **Required.** Zod schema for input validation                                                                                                          |
| `output`       | `z.ZodType`                       | **Required.** Zod schema for output validation                                                                                                         |
| `run`          | `(ctx, input) => Promise<Output>` | **Required.** The implementation function                                                                                                              |
| `integrations` | `Record<string, IntegrationRef>`  | Optional. Integration declarations for the API                                                                                                         |

```typescript
import { api, z } from "@superblocksteam/sdk-api";

const myApi = api({
  // REQUIRED: name is used for identification in logs and debugging
  name: "GreetUser",

  // REQUIRED: Zod schema for input validation
  input: z.object({
    name: z.string(),
    count: z.number().int().positive(),
  }),

  // REQUIRED: Zod schema for output validation
  output: z.object({
    greeting: z.string(),
    items: z.array(z.string()),
  }),

  // REQUIRED: API implementation
  async run(ctx, { name, count }) {
    return {
      greeting: `Hello, ${name}!`,
      items: Array(count).fill("item"),
    };
  },
});
```

### Context (`ctx`) and Input

The `run` function receives two arguments:

1. **`ctx`** - Context object with integrations, logging, environment, and user information
2. **`input`** - Typed, validated input data matching your input schema

```typescript
async run(ctx, { userId, options }) {
  // Input is destructured from the second parameter
  // and is fully typed based on your input schema
}
```

#### `ctx.integrations`

Access typed integration clients via the **name (key)** you declared in the API config: `ctx.integrations.<name>`. For example, if you declared `integrations: { db: postgres(...), notifier: slack(...) }`, use `ctx.integrations.db` and `ctx.integrations.notifier` in your `run` function. Integrations must be declared upfront to enable type-safe access and pre-authentication.

```typescript
import { api, z, postgres, slack, openai, anthropic, stripe, github, notion } from "@superblocksteam/sdk-api";

// Integration IDs from the integrations panel - store in constants
const PROD_POSTGRES = "a1b2c3d4-5678-90ab-cdef-ghijklmnopqr";
const OPS_SLACK = "b2c3d4e5-6789-01ab-cdef-ghijklmnopqr";
const PROD_OPENAI = "c3d4e5f6-7890-12ab-cdef-ghijklmnopqr";
const PROD_ANTHROPIC = "d4e5f6g7-8901-23ab-cdef-ghijklmnopqr";
const PROD_STRIPE = "e5f6g7h8-9012-34ab-cdef-ghijklmnopqr";
const PROD_GITHUB = "f6g7h8i9-0123-45ab-cdef-ghijklmnopqr";
const PROD_NOTION = "g7h8i9j0-1234-56ab-cdef-ghijklmnopqr";

export default api({
  name: "ProcessUserWorkflow",

  // Declare all integrations you'll use
  integrations: {
    // Database
    db: postgres(PROD_POSTGRES),

    // Messaging
    notifier: slack(OPS_SLACK),

    // AI/LLM
    ai: openai(PROD_OPENAI),
    claude: anthropic(PROD_ANTHROPIC),

    // Payment Processing
    payments: stripe(PROD_STRIPE),

    // Developer Tools
    repo: github(PROD_GITHUB),

    // Productivity
    wiki: notion(PROD_NOTION),
  },

  input: z.object({ userId: z.string() }),
  output: z.object({ success: z.boolean() }),

  async run(ctx, { userId }) {
    // Each integration is fully typed based on its declaration
    const users = await ctx.integrations.db.query(...);
    await ctx.integrations.notifier.apiRequest(...);
    const completion = await ctx.integrations.ai.apiRequest(...);

    return { success: true };
  },
});
```

#### `ctx.user`

Access the current user's identity (from the Superblocks JWT). This is the server-side equivalent of the `Global` object available in the frontend.

```typescript
async run(ctx, input) {
  const userId = ctx.user.userId;
  const email = ctx.user.email;       // may be undefined
  const name = ctx.user.name;         // may be undefined
  const groups = ctx.user.groups;     // readonly string[]
  const claims = ctx.user.customClaims; // custom JWT claims

  ctx.log.info('Request from user', { userId, email });
}
```

#### `ctx.log`

Structured logging utilities.

```typescript
async run(ctx, { userId }) {
  ctx.log.info('Processing request', { userId });
  ctx.log.warn('Rate limit approaching');
  ctx.log.error('Operation failed', { error: 'details' });
  ctx.log.debug('Debug info');
}
```

#### `ctx.env`

Access environment variables configured for the application.

```typescript
async run(ctx) {
  const apiKey = ctx.env.EXTERNAL_API_KEY;
  if (!apiKey) {
    throw new Error("EXTERNAL_API_KEY environment variable is not configured");
  }
  const environment = ctx.env.NODE_ENV;
}
```

#### `ctx.dataTag`

The canonical key of the requested data tag that Superblocks validated for the
current execution, such as `"staging"` or `"production"`. This identifies which
integration configuration is in use; it is not proof of a user's environment
entitlement. Combine it with `ctx.user` checks for authorization, and do not
accept a data tag through API input.

```typescript
async run(ctx) {
  // Deny by default, including on older agents where dataTag is undefined.
  if (ctx.dataTag !== "production") {
    throw new Error("This API requires the production data tag");
  }
  // The data tag identifies the integration configuration. User access is
  // checked independently.
  assertPermissions(ctx.user.groups);
}
```

On agents that predate `ctx.dataTag`, the value is `undefined`. Authorization
checks must explicitly deny access for missing or unrecognized tags.

#### `ctx.user`

User information extracted from the Superblocks JWT. This is the **secure, server-side** way to access the current user's identity in API implementations. Never pass user information from the frontend as API input — always use `ctx.user` instead.

```typescript
interface ApiUser {
  /** Unique user identifier from JWT */
  readonly userId: string;

  /** User's email address (if available) */
  readonly email?: string;

  /** User's display name (if available) */
  readonly name?: string;

  /** User's group memberships from JWT */
  readonly groups: readonly string[];

  /** Custom claims from JWT */
  readonly customClaims: Readonly<Record<string, unknown>>;
}
```

##### Basic Usage

```typescript
import { api, z, postgres } from "@superblocksteam/sdk-api";

const PROD_POSTGRES = "a1b2c3d4-5678-90ab-cdef-ghijklmnopqr";

export default api({
  name: "GetMyProfile",
  integrations: { db: postgres(PROD_POSTGRES) },
  input: z.object({}),
  output: z.object({
    email: z.string(),
    name: z.string(),
  }),

  async run(ctx) {
    // Access user identity securely from the JWT — no frontend input needed
    ctx.log.info("Fetching profile", { userId: ctx.user.userId });

    const rows = await ctx.integrations.db.query(
      "SELECT email, display_name FROM users WHERE id = $1",
      z.object({ email: z.string(), display_name: z.string() }),
      [ctx.user.userId],
    );

    if (rows.length === 0) {
      throw new Error("User not found");
    }

    return { email: rows[0].email, name: rows[0].display_name };
  },
});
```

##### Role-Based Access Control with Groups

Use `ctx.user.groups` to restrict API access based on the user's group memberships. Groups are populated from the user's identity provider (e.g., Okta, Azure AD) via the JWT.

```typescript
import { api, z, postgres } from "@superblocksteam/sdk-api";

const PROD_POSTGRES = "a1b2c3d4-5678-90ab-cdef-ghijklmnopqr";

export default api({
  name: "GetAdminDashboard",
  integrations: { db: postgres(PROD_POSTGRES) },
  input: z.object({}),
  output: z.object({
    stats: z.object({
      totalUsers: z.number(),
      activeUsers: z.number(),
    }),
  }),

  async run(ctx) {
    // Check group membership for authorization
    if (!ctx.user.groups.includes("admin")) {
      throw new Error("Access denied: admin group membership required");
    }

    const stats = await ctx.integrations.db.query(
      `SELECT
        COUNT(*) as total_users,
        COUNT(*) FILTER (WHERE status = 'active') as active_users
      FROM users`,
      z.object({ total_users: z.number(), active_users: z.number() }),
    );

    return {
      stats: {
        totalUsers: stats[0].total_users,
        activeUsers: stats[0].active_users,
      },
    };
  },
});
```

##### Custom Claims

`ctx.user.customClaims` contains additional claims from the JWT, such as department, role, or tenant information configured in your identity provider.

**Important limitations:**

- Custom claims are only available when the user is in an **enterprise account that uses SSO**, OR when the application is **deployed and uses embed tokens with SSO claims**.
- In the editor, custom claims will be empty for non-SSO users.
- Always write defensive code that handles missing custom claims gracefully.

```typescript
import { api, z, postgres } from "@superblocksteam/sdk-api";

const PROD_POSTGRES = "a1b2c3d4-5678-90ab-cdef-ghijklmnopqr";

export default api({
  name: "GetTenantData",
  integrations: { db: postgres(PROD_POSTGRES) },
  input: z.object({}),
  output: z.object({
    tenantName: z.string(),
    records: z.array(z.object({ id: z.string(), value: z.string() })),
  }),

  async run(ctx) {
    // Access custom claims from the JWT — handle missing claims defensively
    const tenantId = ctx.user.customClaims["tenant_id"];
    if (typeof tenantId !== "string") {
      throw new Error(
        "Missing tenant_id claim. Custom claims require enterprise SSO or embed tokens with SSO claims.",
      );
    }

    const department = ctx.user.customClaims["department"] as
      | string
      | undefined;

    ctx.log.info("Fetching tenant data", {
      userId: ctx.user.userId,
      tenantId,
      department: department ?? "unknown",
    });

    const records = await ctx.integrations.db.query(
      "SELECT id, value FROM tenant_data WHERE tenant_id = $1",
      z.object({ id: z.string(), value: z.string() }),
      [tenantId],
    );

    const tenantNames = await ctx.integrations.db.query(
      "SELECT name FROM tenants WHERE id = $1",
      z.object({ name: z.string() }),
      [tenantId],
    );

    return {
      tenantName: tenantNames[0]?.name ?? "Unknown Tenant",
      records,
    };
  },
});
```

##### Combining User Context for Row-Level Security

```typescript
import { api, z, postgres } from "@superblocksteam/sdk-api";

const PROD_POSTGRES = "a1b2c3d4-5678-90ab-cdef-ghijklmnopqr";

export default api({
  name: "GetMyOrders",
  integrations: { db: postgres(PROD_POSTGRES) },
  input: z.object({
    status: z.enum(["pending", "completed", "cancelled"]).optional(),
  }),
  output: z.object({
    orders: z.array(
      z.object({
        id: z.string(),
        total: z.number(),
        status: z.string(),
        createdAt: z.string(),
      }),
    ),
  }),

  async run(ctx, { status }) {
    // Use ctx.user for row-level security — users can only see their own orders
    if (!ctx.user.email) {
      throw new Error("User email is required to filter orders");
    }
    const params: unknown[] = [ctx.user.email];
    let query =
      "SELECT id, total, status, created_at FROM orders WHERE user_email = $1";

    if (status) {
      query += " AND status = $2";
      params.push(status);
    }

    query += " ORDER BY created_at DESC LIMIT 50";

    const orders = await ctx.integrations.db.query(
      query,
      z.object({
        id: z.string(),
        total: z.number(),
        status: z.string(),
        created_at: z.string(),
      }),
      params,
    );

    return {
      orders: orders.map((o) => ({
        id: o.id,
        total: o.total,
        status: o.status,
        createdAt: o.created_at,
      })),
    };
  },
});
```

## Integration Clients

### Integration Client Methods Reference

**IMPORTANT:** Before using any integration client, verify available methods. Do NOT assume methods exist based on the external service's API.

#### Common Mistakes to Avoid

---

**WRONG - Hallucinated method:**

```typescript
// These methods DO NOT EXIST
await openai.createChatCompletion({ ... });
await anthropic.createMessage({ ... });
await stripe.createCustomer({ ... });
await github.createIssue({ ... });
await myApi.request({ ... }); // .request() does NOT exist on any client
```

**CORRECT - Use apiRequest:**

```typescript
// Use the generic apiRequest method
await openai.apiRequest(
  { method: 'POST', path: '/chat/completions', body: { ... } },
  { response: ResponseSchema }
);
```

---

**WRONG - Missing schema parameter:**

```typescript
// Schema is REQUIRED for query()
const users = await ctx.integrations.db.query(
  "SELECT * FROM users WHERE id = $1",
  [userId],
);
```

**CORRECT - Include schema:**

```typescript
const UserSchema = z.object({ id: z.string(), name: z.string() });
const users = await ctx.integrations.db.query(
  "SELECT * FROM users WHERE id = $1",
  UserSchema, // Schema is REQUIRED
  [userId],
);
```

### Streaming / SSE

> **Note:** The SDK's `apiRequest()` method does **not** support streaming or Server-Sent Events (SSE) responses. All AI provider calls (OpenAI, Anthropic, Gemini, Groq, Mistral, Cohere, Perplexity, Fireworks) return complete responses — the SDK waits for the full response before returning. If you are building a chat interface that needs real-time token streaming, you will need to handle streaming at the frontend/UI layer rather than through the SDK.

## Generic API Requests

All REST API-based integration clients support the `apiRequest()` method for making custom API calls. **Response schema validation is required** for type safety.

```typescript
import { api, z, slack } from "@superblocksteam/sdk-api";

// Integration ID from the integrations panel
const OPS_SLACK = "b2c3d4e5-6789-01ab-cdef-ghijklmnopqr";

export default api({
  name: "PostSlackMessage",

  integrations: {
    notifier: slack(OPS_SLACK),
  },

  input: z.object({}),
  output: z.object({ ts: z.string() }),

  async run(ctx) {
    // Define request and response schemas
    const PostMessageBodySchema = z.object({
      channel: z.string(),
      text: z.string(),
      blocks: z.array(z.any()).optional(),
    });

    const PostMessageResponseSchema = z.object({
      ts: z.string(),
      channel: z.string(),
    });

    // Response schema is REQUIRED
    const result = await ctx.integrations.notifier.apiRequest(
      {
        method: "POST",
        path: "/chat.postMessage",
        body: {
          channel: "#alerts",
          text: "Deployment completed!",
        },
      },
      {
        body: PostMessageBodySchema,
        response: PostMessageResponseSchema, // Required!
      },
    );

    if (!result.ok) {
      throw new Error(`Slack API error: ${result.error}`);
    }

    // result is fully typed as { ok: true } & PostMessageResponseSchema
    return { ts: result.ts };
  },
});
```

### ApiRequestOptions

| Property   | Type                      | Description                                |
| ---------- | ------------------------- | ------------------------------------------ |
| `method`   | `string`                  | HTTP method (GET, POST, PUT, DELETE, etc.) |
| `path`     | `string`                  | API endpoint path                          |
| `body?`    | `TBody`                   | Request body (for POST, PUT, PATCH)        |
| `params?`  | `Record<string, unknown>` | Query parameters                           |
| `headers?` | `Record<string, string>`  | HTTP headers                               |

### ApiRequestSchema

| Property   | Type                     | Description                                                                    |
| ---------- | ------------------------ | ------------------------------------------------------------------------------ |
| `body?`    | `z.ZodSchema<TBody>`     | Optional Zod schema for request body validation (required if body is provided) |
| `response` | `z.ZodSchema<TResponse>` | **Required** Zod schema for response validation                                |

## Trace Metadata

All integration client methods accept an optional `metadata` parameter as their last argument. When `includeDiagnostics` is enabled, this metadata is captured in the trace view alongside timing and request/response data.

> **Warning:** Do not include secrets, passwords, access tokens, or other sensitive/PII data in trace metadata, as it may be stored in logs or trace payloads and visible to others with access to diagnostics.

```typescript
interface TraceMetadata {
  /** Short human-readable label for this call (e.g., "Fetch active users"). */
  label?: string;
  /** Longer description of what this call does and why. */
  description?: string;
}
```

### Usage

Pass metadata as the last argument to any integration method:

```typescript
// SQL clients (query, execute)
const users = await ctx.integrations.db.query(
  "SELECT * FROM users WHERE status = $1",
  UserSchema,
  ["active"],
  {
    label: "Fetch active users",
    description: "Load users for the dashboard list view",
  },
);

await ctx.integrations.db.execute(
  "UPDATE users SET last_login = NOW() WHERE id = $1",
  [userId],
  { label: "Update last login" },
);

// REST API clients (apiRequest)
const result = await ctx.integrations.slack.apiRequest(
  { method: "POST", path: "/chat.postMessage", body: { channel, text } },
  { body: BodySchema, response: ResponseSchema },
  { label: "Send alert to Slack" },
);

// Other clients (MongoDB, S3, etc.)
const doc = await ctx.integrations.mongo.run(
  "users",
  "findOne",
  ResultSchema,
  { filter: { _id: userId } },
  { label: "Look up user by ID" },
);
```

### When to Use

Trace metadata is optional and purely for observability. Use it when:

- You want labeled traces in the diagnostics panel for easier debugging
- An API makes multiple calls to the same integration and you need to distinguish them
- You want to document the intent of each integration call for your team

Metadata has no effect on execution - it only appears in diagnostics when `includeDiagnostics` is enabled.

## Error Handling

The SDK provides structured error types for different failure scenarios:

```typescript
import {
  InputValidationError,
  OutputValidationError,
  QueryValidationError,
  RestApiValidationError,
  IntegrationNotFoundError,
  IntegrationError,
  ExecutionError,
  ErrorCode,
} from "@superblocksteam/sdk-api";
```

| Error Type                 | Code                    | Description                                        |
| -------------------------- | ----------------------- | -------------------------------------------------- |
| `InputValidationError`     | `INPUT_VALIDATION`      | Input failed Zod validation                        |
| `OutputValidationError`    | `OUTPUT_VALIDATION`     | Output failed Zod validation                       |
| `QueryValidationError`     | N/A                     | Database query result validation failed (Postgres) |
| `RestApiValidationError`   | N/A                     | REST API request/response validation failed        |
| `IntegrationNotFoundError` | `INTEGRATION_NOT_FOUND` | Integration not configured                         |
| `IntegrationError`         | `INTEGRATION_ERROR`     | Integration operation failed                       |
| `ExecutionError`           | `EXECUTION_ERROR`       | User code threw an error                           |

### QueryValidationError (Postgres)

Thrown when database query results fail schema validation. Includes the row index, field-level errors, and the actual row data.

```typescript
try {
  const users = await ctx.integrations.db.query<User>(
    "SELECT * FROM users",
    UserSchema,
    [],
  );
} catch (error) {
  if (error instanceof QueryValidationError) {
    console.error("Validation failed for row:", error.details.rowIndex);
    console.error("Field errors:", error.details.errors);
    console.error("Row data:", error.details.row);
  }
}
```

### RestApiValidationError

Thrown when REST API request bodies or responses fail schema validation. Includes the complete Zod error object with all validation metadata and the actual data that failed.

```typescript
try {
  const result = await ctx.integrations.ai.apiRequest(
    { method: 'POST', path: '/chat/completions', body: { ... } },
    { body: RequestSchema, response: ResponseSchema }
  );
} catch (error) {
  if (error instanceof RestApiValidationError) {
    // Full Zod error with all validation details
    console.error('Zod error:', error.details.zodError);
    console.error('Issues:', error.details.zodError.issues);

    // The actual data that failed validation
    console.error('Failed data:', error.details.data);

    // Access Zod utility methods
    const formatted = error.details.zodError.format();
    const flattened = error.details.zodError.flatten();
  }
}
```

The `RestApiValidationError` provides complete context for debugging:

- **Full Zod error object** (`zodError`) with all validation information
- **Validation codes** (e.g., `"invalid_type"`, `"too_small"`, `"invalid_email"`)
- **Expected vs received** values for type mismatches
- **Constraint information** (e.g., minimum/maximum values)
- **The actual data** that failed validation
- **Utility methods** like `format()` and `flatten()` for different error representations

## Best Practices

### Push Filtering Down to the Data Source

Always filter data at the source (database, API) rather than fetching everything and filtering in code. This reduces network transfer, memory usage, and processing time.

**Inefficient - Fetching all rows and filtering in code:**

```typescript
const AllUsersSchema = z.array(
  z.object({ id: z.string(), status: z.string(), region: z.string() }),
);

// Fetches ALL users, then filters - wasteful!
async run(ctx, { region }) {
  const allUsers = await ctx.integrations.db.query("SELECT * FROM users", AllUsersSchema);
  const activeUsersInRegion = allUsers.filter(
    (u) => u.status === "active" && u.region === region,
  );
}
```

**Efficient - Filtering in the database query:**

```typescript
const UserSchema = z.object({
  id: z.string(),
  status: z.string(),
  region: z.string(),
});

// Database does the filtering - only matching rows are transferred
async run(ctx, { region }) {
  const activeUsersInRegion = await ctx.integrations.db.query(
    "SELECT * FROM users WHERE status = $1 AND region = $2",
    UserSchema,
    ["active", region],
  );
}
```

The same principle applies to REST APIs - use query parameters to filter at the source:

**Inefficient:**

```typescript
// Fetches all issues, then filters client-side
const allIssues = await ctx.integrations.repo.apiRequest(
  { method: "GET", path: "/repos/owner/repo/issues" },
  { response: IssuesSchema },
);
const openBugs = allIssues.filter(
  (i) => i.state === "open" && i.labels.includes("bug"),
);
```

**Efficient:**

```typescript
// API does the filtering - only matching issues returned
const openBugs = await ctx.integrations.repo.apiRequest(
  {
    method: "GET",
    path: "/repos/owner/repo/issues",
    params: { state: "open", labels: "bug" },
  },
  { response: IssuesSchema },
);
```

### Select Only the Fields You Need

Avoid `SELECT *` in database queries. Fetch only the columns you actually use to reduce data transfer and improve query performance.

**Inefficient:**

```typescript
// Fetches all columns even though we only need id and email
const users = await ctx.integrations.db.query(
  "SELECT * FROM users WHERE status = $1",
  UserSchema,
  ["active"],
);
const emails = users.map((u) => u.email);
```

**Efficient:**

```typescript
const UserEmailSchema = z.object({ id: z.string(), email: z.string() });

// Fetches only the columns we need
const users = await ctx.integrations.db.query(
  "SELECT id, email FROM users WHERE status = $1",
  UserEmailSchema,
  ["active"],
);
```

### Use Pagination and Limits

When working with large datasets, use pagination to process data in chunks rather than loading everything into memory.

```typescript
const PageSchema = z.object({
  id: z.string(),
  name: z.string(),
  createdAt: z.string(),
});

// Process in batches of 100
const PAGE_SIZE = 100;
let offset = 0;
let hasMore = true;

while (hasMore) {
  const batch = await ctx.integrations.db.query(
    "SELECT id, name, created_at FROM orders ORDER BY created_at LIMIT $1 OFFSET $2",
    PageSchema,
    [PAGE_SIZE, offset],
  );

  // Process this batch
  for (const order of batch) {
    await processOrder(order);
  }

  hasMore = batch.length === PAGE_SIZE;
  offset += PAGE_SIZE;
}
```

### Avoid N+1 Query Patterns

Don't fetch related data one item at a time in a loop. Use JOINs or batch queries instead.

**N+1 Problem - One query per order:**

```typescript
const orders = await ctx.integrations.db.query(
  "SELECT * FROM orders",
  OrderSchema,
);

// BAD: This makes N additional queries!
for (const order of orders) {
  const items = await ctx.integrations.db.query(
    "SELECT * FROM order_items WHERE order_id = $1",
    ItemSchema,
    [order.id],
  );
  order.items = items;
}
```

**Efficient - Single query with JOIN:**

```typescript
const OrderWithItemsSchema = z.object({
  orderId: z.string(),
  orderTotal: z.number(),
  itemId: z.string(),
  itemName: z.string(),
  quantity: z.number(),
});

// Single query fetches everything
const rows = await ctx.integrations.db.query(
  `SELECT o.id as order_id, o.total as order_total,
          i.id as item_id, i.name as item_name, i.quantity
   FROM orders o
   JOIN order_items i ON i.order_id = o.id`,
  OrderWithItemsSchema,
);
```

**Alternative - Batch fetch with IN clause:**

```typescript
const orders = await ctx.integrations.db.query(
  "SELECT * FROM orders",
  OrderSchema,
);
const orderIds = orders.map((o) => o.id);

// Single query for all items
const items = await ctx.integrations.db.query(
  "SELECT * FROM order_items WHERE order_id = ANY($1)",
  ItemSchema,
  [orderIds],
);
```

### Batch API Requests When Possible

Some APIs support batch operations. Use them instead of making individual requests.

**Inefficient - Individual requests:**

```typescript
// Makes 100 separate API calls!
for (const userId of userIds) {
  await ctx.integrations.notifier.apiRequest(
    {
      method: "POST",
      path: "/chat.postMessage",
      body: { channel: userId, text: "Hello!" },
    },
    { response: MessageSchema },
  );
}
```

**Efficient - Use batch endpoints when available:**

```typescript
// Check if the API supports batch operations
// For Slack, consider using chat.postMessage with multiple channel webhooks
// or processing in controlled parallel batches
const BATCH_SIZE = 10;
for (let i = 0; i < userIds.length; i += BATCH_SIZE) {
  const batch = userIds.slice(i, i + BATCH_SIZE);
  await Promise.all(
    batch.map((userId) =>
      ctx.integrations.notifier.apiRequest(
        {
          method: "POST",
          path: "/chat.postMessage",
          body: { channel: userId, text: "Hello!" },
        },
        { response: MessageSchema },
      ),
    ),
  );
}
```

### Use Precise Schemas

Define schemas that match exactly what you expect. This improves type safety and catches data issues early.

```typescript
// Be specific about field types and constraints
const UserSchema = z.object({
  id: z.string().uuid(),
  email: z.string().email(),
  age: z.number().int().min(0).max(150),
  status: z.enum(["active", "inactive", "pending"]),
  createdAt: z.string().datetime(),
});

// Use .pick() or .omit() to create variants
const UserSummarySchema = UserSchema.pick({ id: true, email: true });
const UserUpdateSchema = UserSchema.omit({ id: true, createdAt: true });
```

## Using APIs in the Frontend

The app template provides automatic type inference for API calls using a tRPC-style pattern.

### Adding a New API (2 Steps)

1. Add a module under `server/apis/` (any layout; see [export styles](#export-style-default-vs-named-exports)).
2. Register it in `server/apis/index.ts` (`.js` specifiers for ESM):

```typescript
import CreateOrder from "./orders/create-order.js";
import GetUsers from "./users/get-users.js";
import { ListProfiles, UpdateProfile } from "./users/pair.js";

const apis = { CreateOrder, GetUsers, ListProfiles, UpdateProfile } as const;

export default apis;
export type ApiRegistry = typeof apis;
```

### Calling APIs from React Components

Import `useApi` from the template's pre-configured hook:

```typescript
import { useApi } from '@/hooks/useApi.js';

function MyComponent() {
  // ✅ Full type inference - input and output types are automatic
  const { run } = useApi("GetUsers");

  const handleClick = async () => {
    // TypeScript knows the exact input type required
    const result = await run({ email: "test@example.com", name: null });

    if (result) {
      // TypeScript knows result.users exists with correct shape
      console.log(result.users);
    }
  };

  return <button onClick={handleClick}>Fetch Users</button>;
}
```

### How It Works

The template includes two files that work together:

1. **`server/apis/index.ts`** - Registry you update when adding APIs
2. **`client/hooks/useApi.ts`** - Pre-configured typed hook (don't modify)

The hook uses `import type` to pull in only the type information from the registry, keeping server code out of the client bundle.

**Key benefits**:

- **Single source of truth**: Just update `server/apis/index.ts`
- **No server code in client bundle**: Type-only imports are erased at compile time
- **Full type safety**: Autocomplete for API names, type errors for invalid inputs/outputs

### Alternative: Explicit Type Parameter

When you cannot use the registry hook, pass the compiled API type:

```typescript
import { useApi } from "@superblocksteam/library";
import type GetUsersApi from "../../server/apis/users/get-users.js";
import type { ListProfiles } from "../../server/apis/users/pair.js";

const getUsers = useApi<typeof GetUsersApi>("GetUsers");
const listProfiles = useApi<typeof ListProfiles>("ListProfiles");
```

## Complete Example

```typescript
import { api, z, postgres, slack, openai } from "@superblocksteam/sdk-api";

// Integration IDs from the integrations panel
const PROD_POSTGRES = "a1b2c3d4-5678-90ab-cdef-ghijklmnopqr";
const OPS_SLACK = "b2c3d4e5-6789-01ab-cdef-ghijklmnopqr";
const PROD_OPENAI = "c3d4e5f6-7890-12ab-cdef-ghijklmnopqr";

// Define reusable schemas
const OrderItemSchema = z.object({
  productId: z.string(),
  quantity: z.number().int().positive(),
});

const CompletionRequestSchema = z.object({
  model: z.string(),
  messages: z.array(
    z.object({
      role: z.string(),
      content: z.string(),
    }),
  ),
});

const CompletionResponseSchema = z.object({
  choices: z.array(
    z.object({
      message: z.object({
        content: z.string(),
      }),
    }),
  ),
});

const CustomerSchema = z.object({
  id: z.string(),
  email: z.string().email(),
});

const SlackPostMessageResponseSchema = z.object({
  ts: z.string().optional(),
});

export default api({
  name: "CreateOrder",

  // Declare all integrations upfront
  integrations: {
    db: postgres(PROD_POSTGRES),
    notifier: slack(OPS_SLACK),
    ai: openai(PROD_OPENAI),
  },

  input: z.object({
    customerId: z.string().uuid(),
    items: z.array(OrderItemSchema).min(1),
    sendAiSummary: z.boolean().default(false),
  }),

  output: z.object({
    orderId: z.string(),
    total: z.number(),
    itemCount: z.number(),
    aiSummary: z.string().optional(),
  }),

  async run(ctx, { customerId, items, sendAiSummary }) {
    ctx.log.info("Creating order", { customerId, itemCount: items.length });

    // Get customer from database
    const customers = await ctx.integrations.db.query(
      "SELECT id, email FROM customers WHERE id = $1",
      CustomerSchema,
      [customerId],
    );
    const customer = customers[0];

    if (!customer) {
      throw new Error(`Customer not found: ${customerId}`);
    }

    // Calculate total
    const total = items.reduce((sum, item) => {
      // In real code, you'd look up prices
      return sum + item.quantity * 10;
    }, 0);

    // Insert order
    await ctx.integrations.db.execute(
      "INSERT INTO orders (customer_id, total, item_count) VALUES ($1, $2, $3)",
      [customerId, total, items.length],
    );

    const orderId = `ord_${Date.now()}`;

    // Notify via Slack
    const slackResult = await ctx.integrations.notifier.apiRequest(
      {
        method: "POST",
        path: "/chat.postMessage",
        body: {
          channel: "#orders",
          text: `New order ${orderId} from ${customer.email}: $${total.toFixed(2)}`,
        },
      },
      { response: SlackPostMessageResponseSchema },
    );

    if (!slackResult.ok) {
      throw new Error(`Slack API error: ${slackResult.error}`);
    }

    // Generate AI summary if requested
    let aiSummary: string | undefined;
    if (sendAiSummary) {
      const completion = await ctx.integrations.ai.apiRequest(
        {
          method: "POST",
          path: "/chat/completions",
          body: {
            model: "gpt-4",
            messages: [
              {
                role: "user",
                content: `Summarize this order: ${items.length} items, total $${total}`,
              },
            ],
          },
        },
        {
          body: CompletionRequestSchema,
          response: CompletionResponseSchema,
        },
      );

      aiSummary = completion.choices[0]?.message.content;
    }

    return {
      orderId,
      total,
      itemCount: items.length,
      aiSummary,
    };
  },
});
```

## Development

```bash
# Install dependencies
pnpm install

# Type check
pnpm typecheck

# Run tests
pnpm test

# Build
pnpm build
```

## License

Superblocks Community Software License
