| name | fmdapi-client |
| description | DataApi factory, OttoAdapter (dk_ API key), FetchAdapter (username/password with token stores), CRUD methods: list, listAll, find, findOne, findFirst, maybeFindFirst, findAll, create, update, delete, get, executeScript, containerUpload, Standard Schema validation, portal data access, FileMaker Data API, layout-bound clients, schema inference
|
| metadata | {"type":"core","library":"proofkit","library_version":"5.2.0"} |
| requires | ["typegen-fmdapi"] |
| sources | ["proofsh/proofkit:packages/fmdapi/src/client.ts","proofsh/proofkit:packages/fmdapi/src/adapters/otto.ts","proofsh/proofkit:packages/fmdapi/src/adapters/fetch.ts","proofsh/proofkit:apps/docs/content/docs/fmdapi/*.mdx"] |
Setup
Start with typegen. Before manually configuring a DataApi client, use npx @proofkit/typegen@beta to generate type-safe clients from your FileMaker layouts. See the typegen-fmdapi skill. Manual setup below is only needed for advanced or non-typegen workflows.
Install the package:
pnpm add @proofkit/fmdapi
OttoAdapter (recommended)
Requires OttoFMS installed on the FileMaker Server. No token management needed — the proxy handles sessions.
import { DataApi, OttoAdapter } from "@proofkit/fmdapi";
const client = DataApi({
adapter: new OttoAdapter({
auth: { apiKey: process.env.OTTO_API_KEY as `dk_${string}` },
db: process.env.FM_DATABASE,
server: process.env.FM_SERVER,
}),
layout: "API_Contacts",
});
API keys must start with dk_ (OttoFMS) or KEY_ (Otto v3). OttoFMS keys use the default HTTPS port with an /otto path prefix. Otto v3 keys use port 3030 by default (configurable via auth.ottoPort).
FetchAdapter (direct Data API)
Authenticates with username/password. Manages Data API session tokens automatically.
import { DataApi, FetchAdapter } from "@proofkit/fmdapi";
const client = DataApi({
adapter: new FetchAdapter({
auth: {
username: process.env.FM_USERNAME,
password: process.env.FM_PASSWORD,
},
db: process.env.FM_DATABASE,
server: process.env.FM_SERVER,
tokenStore: fileTokenStore(),
}),
layout: "API_Contacts",
});
With typegen-generated clients
The recommended path is to use @proofkit/typegen to generate layout-specific clients with full type safety and schema validation. The generated client file exports a pre-configured DataApi instance per layout:
import { CustomersLayout } from "./schema/client";
const { data } = await CustomersLayout.findOne({
query: { id: "==abc123" },
});
Core Patterns
CRUD Operations
Every DataApi client is bound to a single layout. All methods operate on that layout.
Find records:
const response = await client.find({
query: { city: "Portland" },
});
const response = await client.find({
query: [{ city: "Portland" }, { city: "Seattle" }],
});
const { data } = await client.findOne({
query: { email: "==user@example.com" },
});
const { data } = await client.findFirst({
query: { status: "Active" },
});
const result = await client.maybeFindFirst({
query: { email: "==user@example.com" },
});
const allRecords = await client.findAll({
query: { status: "==Active" },
});
const response = await client.find({
query: { email: "==nonexistent@example.com" },
ignoreEmptyResult: true,
});
List records (no find criteria):
const response = await client.list({
sort: [{ fieldName: "lastName", sortOrder: "ascend" }],
limit: 50,
offset: 1,
});
const allRecords = await client.listAll();
Create:
const { recordId, modId } = await client.create({
fieldData: {
firstName: "Jane",
lastName: "Doe",
email: "jane@example.com",
},
});
Update:
await client.update({
recordId: 42,
fieldData: { email: "new@example.com" },
modId: 5,
});
Delete:
await client.delete({ recordId: 42 });
Get by record ID:
const response = await client.get({ recordId: 42 });
Script Execution
const result = await client.executeScript({
script: "Process Order",
scriptParam: JSON.stringify({ orderId: "12345" }),
});
console.log(result.scriptResult);
const { recordId, scriptResult } = await client.create({
fieldData: { name: "New Record" },
script: "After Create Hook",
"script.param": JSON.stringify({ notify: true }),
});
Portal Data
Portal data is returned on every record in the portalData property. Each portal row includes its own recordId and modId.
type TOrderRow = {
"Orders::orderId": string;
"Orders::orderDate": string;
"Orders::total": number;
};
type TPortals = {
portal_orders: TOrderRow;
};
const client = DataApi<TContact, TPortals>({
adapter: new OttoAdapter({ }),
layout: "API_Contacts",
});
const { data } = await client.find({ query: { id: "==123" } });
for (const row of data) {
for (const order of row.portalData.portal_orders) {
console.log(order["Orders::orderId"], order.recordId);
}
}
const response = await client.list({
portalRanges: {
portal_orders: { offset: 1, limit: 10 },
},
});
Container Upload
const file = new Blob(["file contents"], { type: "text/plain" });
await client.containerUpload({
recordId: 42,
containerFieldName: "photo",
file,
containerFieldRepetition: 1,
});
Schema Validation
Note: Schema validators are typically generated by @proofkit/typegen. Manual schemas are only needed for non-typegen setups. If using typegen, customize via override files (see typegen-setup skill).
The schema option accepts any Standard Schema compliant validator (Zod, Valibot, ArkType, etc.). When set, every read method validates and transforms each record's fieldData (and optionally portalData).
import { z } from "zod/v4";
import { DataApi, OttoAdapter } from "@proofkit/fmdapi";
const ZContact = z.object({
firstName: z.string(),
lastName: z.string(),
active: z.coerce.boolean(),
});
const client = DataApi({
adapter: new OttoAdapter({ }),
layout: "API_Contacts",
schema: {
fieldData: ZContact,
},
});
const { data } = await client.findFirst({ query: { id: "==123" } });
If validation fails, the client throws with details about which fields mismatched. This catches FileMaker field renames at runtime before they corrupt downstream logic.
Common Mistakes
CRITICAL: Creating DataApi without an adapter
Wrong:
import { DataApi } from "@proofkit/fmdapi";
const client = DataApi({
layout: "Contacts",
server: "https://fm.example.com",
db: "MyDB.fmp12",
auth: { apiKey: "dk_abc123" },
});
Correct:
import { DataApi, OttoAdapter } from "@proofkit/fmdapi";
const client = DataApi({
adapter: new OttoAdapter({
server: "https://fm.example.com",
db: "MyDB.fmp12",
auth: { apiKey: "dk_abc123" as `dk_${string}` },
}),
layout: "Contacts",
});
v5 requires an explicit adapter instance. Connection config (server, db, auth) goes on the adapter, not DataApi. layout goes on DataApi.
CRITICAL: Omitting token store in production (FetchAdapter)
Wrong:
const client = DataApi({
adapter: new FetchAdapter({
auth: { username: "admin", password: "pass" },
db: "MyDB.fmp12",
server: "https://fm.example.com",
}),
layout: "Contacts",
});
Correct:
import { fileTokenStore } from "@proofkit/fmdapi/tokenStore/file";
const client = DataApi({
adapter: new FetchAdapter({
auth: { username: "admin", password: "pass" },
db: "MyDB.fmp12",
server: "https://fm.example.com",
tokenStore: fileTokenStore(),
}),
layout: "Contacts",
});
Default memoryStore loses tokens on process restart, creating a new session each time. FileMaker allows max 500 concurrent sessions — serverless/edge deployments exhaust this quickly. Use fileTokenStore() for persistent servers or upstashTokenStore() for edge/serverless. OttoAdapter avoids this entirely.
HIGH: Storing FM recordId as a persistent identifier
Wrong:
const { recordId } = await client.create({ fieldData: { name: "Acme" } });
await myDb.insert({ fmRecordId: recordId });
Correct:
const { data } = await client.findOne({ query: { name: "Acme" } });
const stableId = data.fieldData.primaryKey;
FileMaker's internal recordId can change during imports, migrations, or file recovery. Always use a dedicated primary key field (UUID or serial) for cross-system references. Only use recordId for immediate operations (update/delete) within the same request flow.
HIGH: Assuming dynamic layout switching on a single client
Wrong:
const client = DataApi({
adapter: new OttoAdapter({ }),
layout: "Contacts",
});
await client.find({ layout: "Invoices", query: { status: "Open" } });
Correct:
const contactsClient = DataApi({
adapter: new OttoAdapter({ }),
layout: "Contacts",
});
const invoicesClient = DataApi({
adapter: new OttoAdapter({ }),
layout: "Invoices",
});
Each DataApi client is bound to one layout at creation. v5 removed per-method layout override. Create a separate client per layout. The adapter instance can be shared.
MEDIUM: Using wrong Otto API key format
Wrong:
new OttoAdapter({
auth: { apiKey: "abc123-def456" },
db: "MyDB.fmp12",
server: "https://fm.example.com",
});
Correct:
new OttoAdapter({
auth: { apiKey: "dk_abc123def456" as `dk_${string}` },
db: "MyDB.fmp12",
server: "https://fm.example.com",
});
OttoFMS keys start with dk_, Otto v3 keys start with KEY_. The adapter uses this prefix to determine the connection method (port 3030 for KEY_, /otto path prefix for dk_). An unrecognized prefix throws at construction time.
HIGH: Using deprecated zodValidators option instead of schema
Wrong:
import { z } from "zod";
const client = DataApi({
adapter: new OttoAdapter({ }),
layout: "Contacts",
zodValidators: {
fieldData: z.object({ name: z.string() }),
},
});
Correct:
import { z } from "zod/v4";
const client = DataApi({
adapter: new OttoAdapter({ }),
layout: "Contacts",
schema: {
fieldData: z.object({ name: z.string() }),
},
});
zodValidators was removed in v5. Use schema instead, which accepts any Standard Schema compliant validator. If upgrading from v4, re-run npx @proofkit/typegen to regenerate clients with the new option. The client throws at runtime if zodValidators is passed.
CRITICAL: Manually redefining TypeScript types instead of generated types
Wrong:
type Contact = {
firstName: string;
lastName: string;
email: string;
};
const client = DataApi<Contact>({
adapter: new OttoAdapter({ }),
layout: "API_Contacts",
});
Correct:
import { ContactsLayout } from "./schema/client";
const { data } = await ContactsLayout.find({ query: { email: "==test@example.com" } });
Manual types drift when FileMaker fields change, with no runtime protection. The typegen-generated client bundles a Standard Schema validator that catches field renames at runtime. Run npx @proofkit/typegen after any layout change. See typegen-setup skill for more details.
HIGH: Mixing Zod v3 and v4 in the same project
Wrong:
import { z } from "zod";
import { z as z4 } from "zod/v4";
Correct:
import { z } from "zod/v4";
Zod v3 and v4 have different Standard Schema implementations. Mixing them causes subtle type mismatches and potential runtime validation failures. The typegen tool generates zod/v4 imports by default. See typegen-setup skill for more details.
References
- typegen-fmdapi -- type generation and client scaffolding that produces the layout-specific clients referenced above. This is the recommended entry point for new projects.
- fmodata-client -- alternative ORM-style client using the OData API (Drizzle-like query builder, different from the REST-based Data API covered here)