# REST API Integration Client

Make HTTP requests to any REST API configured with a base URL and authentication in the integrations page.

This is the generic REST API Integration plugin. Unlike named integrations (Slack, GitHub, etc.) which target a specific service, this plugin works with **any** HTTP API you configure as an integration.

## Methods

| Method                                   | Description                                  |
| ---------------------------------------- | -------------------------------------------- |
| `apiRequest(options, schema, metadata?)` | Make any HTTP request with schema validation |

## Usage

### Basic GET Request

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

// Integration ID from the integrations panel
const MY_API = "a1b2c3d4-5678-90ab-cdef-111111111111";

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

export default api({
  name: "FetchUsers",
  integrations: {
    myApi: restApiIntegration(MY_API),
  },
  input: z.object({}),
  output: z.object({
    users: z.array(
      z.object({ id: z.string(), name: z.string(), email: z.string() }),
    ),
  }),
  async run(ctx) {
    const result = await ctx.integrations.myApi.apiRequest(
      {
        method: "GET",
        path: "/users",
        params: { limit: 50 },
      },
      { response: UsersResponseSchema },
    );

    return { users: result.users };
  },
});
```

### POST Request with Body

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

const record = await ctx.integrations.myApi.apiRequest(
  {
    method: "POST",
    path: "/records",
    body: {
      title: "New Record",
      description: "Created via Superblocks",
      tags: ["automated"],
    },
    headers: {
      "Content-Type": "application/json",
    },
  },
  { response: CreateRecordSchema },
);

console.log(`Created record: ${record.id}`);
```

### PATCH/PUT Request

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

await ctx.integrations.myApi.apiRequest(
  {
    method: "PATCH",
    path: `/records/${recordId}`,
    body: {
      title: "Updated Title",
      status: "published",
    },
    headers: {
      "Content-Type": "application/json",
    },
  },
  { response: UpdateResponseSchema },
);
```

### DELETE Request

```typescript
await ctx.integrations.myApi.apiRequest(
  {
    method: "DELETE",
    path: `/records/${recordId}`,
  },
  { response: z.object({}).passthrough() },
);
```

### Search with Query Parameters

```typescript
const SearchResponseSchema = z.object({
  items: z.array(
    z.object({
      id: z.string(),
      title: z.string(),
      score: z.number().optional(),
    }),
  ),
  total: z.number(),
});

const results = await ctx.integrations.myApi.apiRequest(
  {
    method: "GET",
    path: "/search",
    params: {
      q: "my query",
      page: 1,
      per_page: 25,
    },
  },
  { response: SearchResponseSchema },
);

console.log(`Found ${results.total} results`);
```

### Using Multiple Integrations

You can use multiple REST API Integrations in a single API, each targeting a different service:

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

const EXTERNAL_API = "a1b2c3d4-5678-90ab-cdef-111111111111";
const INTERNAL_API = "e5f6a7b8-9012-34cd-ef56-222222222222";
const MY_DB = "c3d4e5f6-7890-12ab-cdef-333333333333";

export default api({
  name: "SyncData",
  integrations: {
    externalApi: restApiIntegration(EXTERNAL_API),
    internalApi: restApiIntegration(INTERNAL_API),
    db: postgres(MY_DB),
  },
  input: z.object({ query: z.string() }),
  output: z.object({ synced: z.number() }),
  async run(ctx, { query }) {
    // Fetch from external API
    const external = await ctx.integrations.externalApi.apiRequest(
      { method: "GET", path: "/data", params: { q: query } },
      { response: z.object({ items: z.array(z.unknown()) }) },
    );

    // Push to internal API
    await ctx.integrations.internalApi.apiRequest(
      { method: "POST", path: "/import", body: { items: external.items } },
      { response: z.object({ imported: z.number() }) },
    );

    return { synced: external.items.length };
  },
});
```

## Trace Metadata

All methods accept an optional `metadata` parameter as the last argument for diagnostics labeling. See the [root SDK README](../../../README.md#trace-metadata) for details.

```typescript
const result = await ctx.integrations.myApi.apiRequest(
  { method: "GET", path: "/users" },
  { response: UsersResponseSchema },
  { label: "myApi.getUsers", description: "Fetch all active users" },
);
```

## Common Pitfalls

### No `.request()` Method

The only public method is `apiRequest()`. There is no `.request()` method:

```typescript
// WRONG - .request() does not exist
await ctx.integrations.myApi.request({
  method: "GET",
  path: "/users",
});

// CORRECT - Use apiRequest with a response schema
await ctx.integrations.myApi.apiRequest(
  { method: "GET", path: "/users" },
  { response: UsersResponseSchema },
);
```

### Response Schema is Required

`apiRequest()` requires a response schema for type safety:

```typescript
// WRONG - Missing response schema
const result = await ctx.integrations.myApi.apiRequest({
  method: "GET",
  path: "/users",
});

// CORRECT - Provide a response schema
const result = await ctx.integrations.myApi.apiRequest(
  { method: "GET", path: "/users" },
  { response: z.object({ users: z.array(z.unknown()) }) },
);
```

### Use `body` Instead of `JSON.stringify`

The `body` field is automatically serialized. Do not stringify it manually:

```typescript
// WRONG - Manual stringify
await ctx.integrations.myApi.apiRequest(
  {
    method: "POST",
    path: "/records",
    body: JSON.stringify({ title: "New" }),
  },
  { response: ResponseSchema },
);

// CORRECT - Pass object directly
await ctx.integrations.myApi.apiRequest(
  {
    method: "POST",
    path: "/records",
    body: { title: "New" },
  },
  { response: ResponseSchema },
);
```

### Use `params` for Query Parameters

Query parameters go in the `params` field, not in the URL path:

```typescript
// WRONG - Query params in path
await ctx.integrations.myApi.apiRequest(
  { method: "GET", path: "/search?q=hello&limit=10" },
  { response: ResponseSchema },
);

// CORRECT - Use params field
await ctx.integrations.myApi.apiRequest(
  {
    method: "GET",
    path: "/search",
    params: { q: "hello", limit: 10 },
  },
  { response: ResponseSchema },
);
```

### Use `.passthrough()` for Flexible Schemas

When the response contains fields you don't need to validate, use `.passthrough()`:

```typescript
// Strict schema - will reject unknown fields
const StrictSchema = z.object({ id: z.string() });

// Flexible schema - allows additional fields
const FlexibleSchema = z.object({ id: z.string() }).passthrough();

// For dynamic responses
const GenericSchema = z.record(z.unknown());
```

## Error Handling

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

try {
  const result = await ctx.integrations.myApi.apiRequest(
    { method: "GET", path: "/users" },
    { response: UsersResponseSchema },
  );
} catch (error) {
  if (error instanceof RestApiValidationError) {
    // Response didn't match the schema
    console.error("Validation failed:", error.details.zodError);
    console.error("Actual response:", error.details.data);
  }
}
```
