| name | attio-local-dev-loop |
| description | Set up a fast local development loop for Attio integrations with
hot reload, mock server, and integration tests.
Trigger: "attio dev setup", "attio local development",
"attio dev environment", "develop with attio", "attio project setup".
|
| allowed-tools | Read, Write, Edit, Bash(npm:*), Bash(pnpm:*), Bash(npx:*), Grep |
| version | 1.6.0 |
| license | MIT |
| author | Jeremy Longshore <jeremy@intentsolutions.io> |
| tags | ["saas","crm","attio"] |
| compatibility | Designed for Claude Code |
Attio Local Dev Loop
Overview
Set up a fast, reproducible local development workflow for Attio REST API integrations. Includes project structure, typed client, mock server for offline work, and integration test harness.
Prerequisites
- Completed
attio-install-auth setup
- Node.js 18+ with npm or pnpm
- TypeScript 5+
Instructions
Step 1: Project Structure
my-attio-integration/
โโโ src/
โ โโโ attio/
โ โ โโโ client.ts # Typed fetch wrapper (see attio-install-auth)
โ โ โโโ types.ts # Attio response types
โ โ โโโ config.ts # Env-based configuration
โ โโโ services/
โ โ โโโ people.ts # People record operations
โ โ โโโ companies.ts # Company record operations
โ โ โโโ lists.ts # List entry operations
โ โโโ index.ts
โโโ tests/
โ โโโ mocks/
โ โ โโโ attio-fixtures.ts # Realistic API response fixtures
โ โโโ unit/
โ โ โโโ people.test.ts
โ โโโ integration/
โ โโโ attio-live.test.ts # Runs against real API (CI only)
โโโ .env.example
โโโ .env.local # Git-ignored, real credentials
โโโ tsconfig.json
โโโ package.json
Step 2: Type the Attio Response Model
export interface AttioRecordId {
object_id: string;
record_id: string;
}
export interface AttioValue<T = unknown> {
active_from: string;
active_until: string | null;
created_by_actor: { type: string; id: string };
attribute_type: string;
[key: string]: T | unknown;
}
export interface AttioRecord {
id: AttioRecordId;
created_at: string;
values: Record<string, AttioValue[]>;
}
export interface AttioListResponse<T> {
data: T[];
pagination?: {
next_cursor?: string;
has_more?: boolean;
};
}
{
: ;
: ;
: ;
: ;
}
Step 3: Environment Configuration
export interface AttioConfig {
apiKey: string;
baseUrl: string;
timeout: number;
environment: "development" | "staging" | "production";
}
export function loadConfig(): AttioConfig {
const env = process.env.NODE_ENV || "development";
return {
apiKey: process.env.ATTIO_API_KEY || "",
baseUrl: process.env.ATTIO_BASE_URL || "https://api.attio.com/v2",
timeout: parseInt(process.env.ATTIO_TIMEOUT || "30000", 10),
environment: env as AttioConfig["environment"],
};
}
Step 4: Package Scripts for Dev Loop
{
"scripts": {
"dev": "tsx watch src/index.ts",
"test": "vitest run",
"test:watch": "vitest --watch",
"test:integration": "ATTIO_LIVE=1 vitest run tests/integration/",
"typecheck": "tsc --noEmit",
"lint": "eslint src/ tests/"
},
"devDependencies": {
"tsx": "^4.0.0",
"vitest": "^2.0.0",
"typescript": "^5.5.0",
"msw": "^2.0.0"
}
}
Step 5: Mock Attio API with MSW
import { http, HttpResponse } from "msw";
import { setupServer } from "msw/node";
const BASE = "https://api.attio.com/v2";
export const handlers = [
http.get(`${BASE}/objects`, () =>
HttpResponse.json({
data: [
{ api_slug: "people", singular_noun: "Person", plural_noun: "People" },
{ api_slug: "companies", singular_noun: "Company", plural_noun: "Companies" },
],
})
),
http.post(`${BASE}/objects/people/records/query`, () =>
HttpResponse.json({
data: [
{
id: { object_id: "obj_people", record_id: "rec_abc123" },
created_at: "2025-01-15T10:00:00.000Z",
values: {
name: [{ : , : , : }],
: [{ : }],
},
},
],
})
),
http.(, ({ request }) => {
body = ( request.()) <, >;
.({
: {
: { : , : },
: ().(),
: (body ).?. || {},
},
}, { : });
}),
];
mockServer = (...handlers);
Step 6: Write Tests Against Mocks
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { mockServer } from "../mocks/attio-fixtures";
import { attioFetch } from "../../src/attio/client";
beforeAll(() => mockServer.listen());
afterAll(() => mockServer.close());
describe("People Service", () => {
it("queries people records", async () => {
const res = await attioFetch<{ data: any[] }>({
method: "POST",
path: "/objects/people/records/query",
body: { limit: 10 },
});
expect(res.data).toHaveLength(1);
expect(res.data[0].values.name[0].full_name).toBe("Ada Lovelace");
});
it("creates a person", async () => {
const res = attioFetch<{ : { : { : } } }>({
: ,
: ,
: {
: { : { : [] } },
},
});
(res...).();
});
});
Step 7: Integration Test (Live API)
import { describe, it, expect } from "vitest";
import { attioFetch } from "../../src/attio/client";
const LIVE = process.env.ATTIO_LIVE === "1";
describe.skipIf(!LIVE)("Attio Live API", () => {
it("lists objects from real workspace", async () => {
const res = await attioFetch<{ data: Array<{ api_slug: string }> }>({
path: "/objects",
});
expect(res.data.map((o) => o.api_slug)).toContain("people");
});
});
Error Handling
| Issue | Cause | Solution |
|---|
fetch is not defined | Node < 18 | Upgrade Node.js or add undici |
| MSW not intercepting | Wrong base URL | Match ATTIO_BASE_URL in mock handlers |
| Integration test fails | Missing/invalid token | Set ATTIO_API_KEY in .env.local |
| TypeScript errors on values | Attio multiselect arrays | Values are always arrays -- type as T[] |
Resources
Next Steps
See attio-sdk-patterns for production-ready client patterns.