Production-ready architecture for Webflow Data API v2 integrations. Layered design separating API access, business logic, caching, and webhook handling.
webflow-api SDK (v3.x)my-webflow-project/
├── src/
│ ├── webflow/ # Webflow API layer
│ │ ├── client.ts # WebflowClient singleton
│ │ ├── types.ts # TypeScript types for Webflow resources
│ │ ├── errors.ts # Custom error classes
│ │ └── cache.ts # Response caching (LRU/Redis)
│ ├── services/ # Business logic layer
│ │ ├── cms.service.ts # CMS content management
│ │ ├── ecommerce.service.ts # Products, orders, inventory
│ │ ├── forms.service.ts # Form submission processing
│ │ └── sync.service.ts # External data sync
│ ├── webhooks/ # Event handling layer
│ │ ├── router.ts # Event type routing
│ │ ├── handlers/
│ │ │ ├── form-submission.ts
│ │ │ ├── cms-item-changed.ts
│ │ │ └── ecomm-new-order.ts
│ │ └── middleware.ts # Signature verification
│ ├── api/ # HTTP endpoints
│ │ ├── health.ts
│ │ ├── webhooks.ts
│ │ └── content.ts
│ └── config/
│ └── webflow.ts # Environment-aware config
├── tests/
│ ├── unit/
│ │ ├── services/
│ │ └── webhooks/
│ └── integration/
│ └── webflow.integration.test.ts
├── .env.example
├── tsconfig.json
└── package.json
┌──────────────────────────────────────────────────┐
│ API Layer │
│ Express routes, webhook endpoints, health │
├──────────────────────────────────────────────────┤
│ Service Layer │
│ CMS sync, ecommerce, form processing │
│ (Business logic, orchestration) │
├──────────────────────────────────────────────────┤
│ Webflow Client Layer │
│ WebflowClient wrapper, error handling, types │
├──────────────────────────────────────────────────┤
│ Infrastructure Layer │
│ Cache (LRU/Redis), queue (p-queue), monitoring │
└──────────────────────────────────────────────────┘
// src/webflow/client.ts
import { WebflowClient } from "webflow-api";
import { getConfig } from "../config/webflow.js";
let client: WebflowClient | null = null;
export function getClient(): WebflowClient {
if (!client) {
const config = getConfig();
client = new WebflowClient({
accessToken: config.accessToken,
maxRetries: config.maxRetries,
});
}
return client;
}
export function resetClient(): void {
client = null;
}
// src/webflow/errors.ts
export class WebflowServiceError extends Error {
constructor(
message: string,
public readonly statusCode: number,
public readonly retryable: boolean,
public readonly originalError?: unknown
) {
super(message);
this.name = "WebflowServiceError";
}
static fromApiError(error: any): WebflowServiceError {
const status = error.statusCode || error.status || 500;
const retryable = status === 429 || status >= 500;
return new WebflowServiceError(
error.message || "Unknown Webflow error",
status,
retryable,
error
);
}
}
// src/webflow/types.ts
export interface WebflowSite {
id: string;
displayName: string;
shortName: string;
lastPublished: string | null;
customDomains?: Array<{ url: string }>;
}
export interface WebflowCollection {
id: string;
displayName: string;
slug: string;
itemCount: number;
fields: WebflowField[];
}
export interface WebflowField {
slug: string;
displayName: string;
type: string;
isRequired: boolean;
}
export interface WebflowItem {
id: string;
isDraft: boolean;
isArchived: boolean;
createdOn: string;
lastUpdated: string;
fieldData: Record<string, any>;
}
// src/services/cms.service.ts
import { getClient } from "../webflow/client.js";
import { WebflowServiceError } from "../webflow/errors.js";
import { cachedFetch, invalidateCache } from "../webflow/cache.js";
import type { WebflowItem } from "../webflow/types.js";
export class CmsService {
private webflow = getClient();
async getCollections(siteId: string) {
return cachedFetch(
`collections:${siteId}`,
() => this.webflow.collections.list(siteId).then(r => r.collections!),
30 * 60 * 1000 // 30 min — schemas change rarely
);
}
async getPublishedItems(collectionId: string): Promise<WebflowItem[]> {
// CDN-cached — no rate limit
return cachedFetch(
`items:live:${collectionId}`,
() => this.webflow.collections.items.listItemsLive(collectionId, { limit: 100 })
.then(r => r.items as WebflowItem[]),
60 * 1000 // 1 min
);
}
async createItems(
collectionId: string,
items: Array<{ fieldData: Record<string, any> }>
): Promise<string[]> {
try {
const result = await this.webflow.collections.items.createItemsBulk(
collectionId,
{ items: items.map(i => ({ ...i, isDraft: false })) }
);
// Invalidate cache after write
invalidateCache(`items:live:${collectionId}`);
return result.items!.map(i => i.id!);
} catch (error) {
throw WebflowServiceError.fromApiError(error);
}
}
async publishItems(collectionId: string, itemIds: string[]): Promise<void> {
await this.webflow.collections.items.publishItem(collectionId, { itemIds });
invalidateCache(`items:live:${collectionId}`);
}
}
// src/services/sync.service.ts
import { CmsService } from "./cms.service.js";
export class SyncService {
constructor(private cms: CmsService) {}
async syncFromExternal(
collectionId: string,
externalData: Array<{ title: string; body: string; slug: string }>
) {
// Get existing items to avoid duplicates
const existing = await this.cms.getPublishedItems(collectionId);
const existingSlugs = new Set(existing.map(i => i.fieldData?.slug));
// Filter new items
const newItems = externalData
.filter(d => !existingSlugs.has(d.slug))
.map(d => ({
fieldData: {
name: d.title,
slug: d.slug,
"post-body": d.body,
},
}));
if (newItems.length === 0) return { synced: 0 };
// Bulk create (100 at a time)
const createdIds = await this.cms.createItems(collectionId, newItems.slice(0, 100));
// Publish new items
await this.cms.publishItems(collectionId, createdIds);
return { synced: createdIds.length };
}
}
// src/webhooks/router.ts
import { handleFormSubmission } from "./handlers/form-submission.js";
import { handleCmsItemChanged } from "./handlers/cms-item-changed.js";
import { handleNewOrder } from "./handlers/ecomm-new-order.js";
type Handler = (payload: any) => Promise<void>;
const handlers: Record<string, Handler> = {
form_submission: handleFormSubmission,
collection_item_created: handleCmsItemChanged,
collection_item_changed: handleCmsItemChanged,
ecomm_new_order: handleNewOrder,
};
export async function routeWebhookEvent(
triggerType: string,
payload: any
): Promise<void> {
const handler = handlers[triggerType];
if (!handler) {
console.log(`No handler for: ${triggerType}`);
return;
}
await handler(payload);
}
// src/webhooks/handlers/cms-item-changed.ts
import { invalidateCache } from "../../webflow/cache.js";
export async function handleCmsItemChanged(payload: any): Promise<void> {
const { collectionId, itemId } = payload;
// Invalidate cache for this collection
invalidateCache(`items:live:${collectionId}`);
invalidateCache(`items:staged:${collectionId}`);
// Trigger downstream updates (search index, external DB, etc.)
console.log(`CMS item changed: ${itemId} in collection ${collectionId}`);
}
// src/config/webflow.ts
interface WebflowConfig {
accessToken: string;
siteId: string;
maxRetries: number;
environment: "development" | "staging" | "production";
webhookSecret: string;
}
export function getConfig(): WebflowConfig {
const env = (process.env.NODE_ENV || "development") as WebflowConfig["environment"];
return {
accessToken: requireEnv("WEBFLOW_API_TOKEN"),
siteId: requireEnv("WEBFLOW_SITE_ID"),
maxRetries: env === "production" ? 3 : 1,
environment: env,
webhookSecret: process.env.WEBFLOW_WEBHOOK_SECRET || "",
};
}
function requireEnv(name: string): string {
const value = process.env[name];
if (!value) throw new Error(`${name} environment variable required`);
return value;
}
External Data Source
│
▼
┌─────────────────┐ ┌─────────────┐
│ Sync Service │────▶│ CMS Service │
│ (orchestration)│ │ (CRUD ops) │
└─────────────────┘ └──────┬───────┘
│
┌──────────┴──────────┐
▼ ▼
┌──────────────┐ ┌────────────────┐
│ Cache (LRU) │ │ Webflow Client │
│ or Redis │ │ (webflow-api) │
└──────────────┘ └───────┬────────┘
│
▼
┌────────────────┐
│ Webflow API v2 │
│ api.webflow.com│
└────────────────┘
| Issue | Cause | Solution |
|---|---|---|
| Circular imports | Wrong layer dependencies | Services depend on client, not reverse |
| Cache inconsistency | Missing invalidation | Invalidate on writes and webhook events |
| Config missing | Environment not set | requireEnv() fails fast with clear message |
| Type mismatches | API shape changes | Update types.ts from collection schema |
For multi-environment setup, see webflow-multi-env-setup.